The Simplified Go Food Web App - Iteration 3

Continue Where We Left Off

Yesterday we have:

  1. Learn how to create unit testing for Model and Controller
  2. Learn how to create Model, Controller, and View from scratch without the help of scaffolding
  3. Developing "Cart" feature to enable our users to add Food(s) to cart

Emptying Cart

We have written features to add foods to cart. But what about removing them from it? Let's start with the requirements:

  1. There should be a button named "empty cart" in your cart
  2. When a user clicks it
    • it should remove only user's own cart
    • and it should remove the cart from user's session
  3. After the cart is removed, user is redirected to Store index

Write Your Specs

Based on requirements above, modify Carts controller spec for "destroy" method!

Carts Controller Spec (1)

Your modified spec should look like this.

describe CartsController do
  # -cut-
  describe "DELETE destroy" do
    before :each do
      @cart = create(:cart)
      session[:cart_id] = @cart.id
    end
    # -cut-
  end
end

Carts Controller Spec (2)

Your modified spec should look like this.

describe CartsController do
  # -cut-
  describe "DELETE destroy" do
    # -cut-
    context "with valid cart id" do
      it "destroys the requested cart" do
        expect {
          delete :destroy, params: { id: @cart.id }, session: valid_session
        }.to change(Cart, :count).by(-1)
      end

      it "removes the cart from user's session" do
        delete :destroy, params: { id: @cart.id }, session: valid_session
        expect(session[:id]).to eq(nil)
      end

      it "redirects to the store home page" do
        delete :destroy, params: { id: @cart.id }, session: valid_session
        expect(response).to redirect_to(store_index_url)
      end
    end
    # -cut-
  end
end

Carts Controller Spec (3)

Your modified spec should look like this.

describe CartsController do
  # -cut-
  describe "DELETE destroy" do
    # -cut-
    context "with invalid cart id" do
      it "does not destroy the requested cart" do
        other_cart = create(:cart)

        expect{
          delete :destroy, params: { id: other_cart.id }, session: valid_session
        }.not_to change(Cart, :count)
      end
    end
  end
end

Make It Pass (1)

Now try to modify your cart's "destroy" method to make it pass.

Make It Pass (2)

Your cart's destroy method should look like this.

class CartsController < ApplicationController
  # -cut-
  def destroy
    @cart.destroy if @cart.id == session[:cart_id]
    session[:cart_id] == nil

    respond_to do |format|
      format.html { redirect_to store_index_url, notice: 'Cart was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
end

Write The View

Add the "empty cart" button to your view.

<p id="notice"><%= notice %></p>

<h2>Your Cart</h2>

<table>
  <% @cart.line_items.each do |item| %>
    <tr>
      <td><%= item.quantity %> ×</td>
      <td><%= item.food.name %></td>
      <td class="item_price"><%= number_to_currency(item.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
    </tr>
  <% end %>
  <tr class="total_line">
    <td colspan="2">Total</td>
    <td class="total_cell"><%= number_to_currency(@cart.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
  </tr>
</table>

<%= button_to 'Empty cart', @cart, method: :delete, data: { confirm: 'Are you sure?' } %>

Commit!

Play around with your app and commit your progress.

Ajax-Based Cart

What is Ajax?

Asynchronous Javascript and XML

 

Ajax is a combination of:

  • A browser built-in XMLHttpRequest object (to request data from a web server)
  • JavaScript and HTML DOM (to display or use the data)

But What does It Do, Exactly?

Ajax allows web pages to be updated asynchronously by exchanging data with a web server behind the scenes. This means that it is possible to update parts of a web page, without reloading the whole page.

- w3schools.com

Ajax in Your Daily Life

  • Notifications
  • Autocomplete Search
  • Live Chat
  • In Place Submissions
  • State Altering Drag and Drop
  • etc...

Ajax in Your App

It's your product manager again! Looking at your progress, he is excited to add more features to your app. Yeay... I guess?

Let's go to the requirements:

  1. The cart should appear in the sidebar in every page
  2. The cart should be able to be updated without reloading the whole page

Partial Templates (1)

We are going to move our cart to the sidebar. But before that, we will make use of partial templates. Let's modify our show cart page first.

<p id="notice"><%= notice %></p>

<h2>Your Cart</h2>

<table>
  <%= render (@cart.line_items) %>
  
  <tr class="total_line">
    <td colspan="2">Total</td>
    <td class="total_cell"><%= number_to_currency(@cart.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
  </tr>
</table>

<%= button_to 'Empty cart', @cart, method: :delete, data: { confirm: 'Are you sure?' } %>

Partial Templates (2)

The "render" method in our previous code will iterate over any collection that is passed to it. A partial template is simply another template file. However, Rails automatically prepends an underscore to the partial name when looking for the file. Following Rails convention, we should name our partial "_line_item.html.erb" and put it in "app/views/line_items" folder.

<tr>
  <td><%= line_item.quantity %> ×</td>
  <td><%= line_item.food.name %></td>
  <td class="item_price"><%= number_to_currency(line_item.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
</tr>

Also note that in the partial file, we refer to the current object by using the variable name that matches the name of the template, "line_item".

Partial Templates (3)

Next, because we want to use the same cart template both in show cart page and in the sidebar, we want to be able to render the whole cart as a partial. So, we will change our show cart page again.

<p id="notice"><%= notice %></p>

<h2>Your Cart</h2>

<%= render @cart %>

Can you guess where to put the lines of code that we just removed from our show cart page?

Partial Templates (4)

You should have guessed it easily, we put our partial in a file named "_cart.html.erb" in "app/views/carts/" folder.

<table>
  <%= render (@cart.line_items) %>
  
  <tr class="total_line">
    <td colspan="2">Total</td>
    <td class="total_cell"><%= number_to_currency(@cart.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
  </tr>
</table>

<%= button_to 'Empty cart', @cart, method: :delete, data: { confirm: 'Are you sure?' } %>

Partial Templates (5)

Now we can render our cart partial from the sidebar.

<!DOCTYPE html>
<html>
  <!-- cut -->
  <body class="<%= controller.controller_name %>">
    <!-- cut -->
    <div id="columns">
      <div id="side">
        <div id=cart>
          <%= render @cart %>
        </div>
        
        <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">My Account</a></li>
          <li><a href="#">My Orders</a></li>
        </ul>
      </div>
      <div id="main">
        <%= yield %>
      </div>
    </div>
  </body>
</html>

Modifying The Stylesheet (1)

We now have to modify our stylesheet for the cart.

.carts, #side #cart {
  .item_price, .total_line {
    text-align: right;
  }

  .total_line, .total_cell {
    font-weight: bold;
    border-top: 1px solid #595;
  }
}

Modifying The Stylesheet (2)

Also our application stylesheet.

/* cut */
#columns {
  /* cut */
  #side {
    padding: 1em 2em;
    background: #141;

    form, div {
      display: inline;
    }

    input {
      font-size: small;
    }

    #cart {
      font-size: smaller;
      color: white;

      table {
        border-top: 1px dotted #595;
        border-bottom: 1px dotted #595;
        margin-bottom: 10px;
      }
    }
    /* cut */
  }
  /* cut */
}

Modifying Store Controller Spec

We need to change our store controller spec to include CurrentCart module. We have done this before with LineItems controller, so you should be able to write the specs and the modification to the controller already. Write them!

Store Controller and Spec

This how your store controller spec should look like.

require 'rails_helper'

describe StoreController do
  describe "GET index" do
    # -cut-
    it "includes CurrentCart" do
      expect(StoreController.ancestors.include? CurrentCart).to eq(true)
    end
  end
end

And this how your store controller should look like.

class StoreController < ApplicationController
  include CurrentCart
  before_action :set_cart

  def index
    @foods = Food.order(:name)
  end
end

Modifying LinteItems Controller Spec

This also should be easy for you. Change our create method in LineItems controller spec to redirect to Store index page after finish executing its job. Write the spec and the modified controller!

LineItems Controller and Spec

This how your LineItems controller spec should look like.

require 'rails_helper'

describe LineItemsController do
  describe 'POST #create' do
    # -cut-
    it "redirects to store#index" do
      post :create, params: { food_id: @food.id }
      expect(response).to redirect_to(store_index_url)
    end
  end
end

LineItems Controller

And this how your LineItems controller should look like.

class LineItemsController < ApplicationController
  # -cut-
  def create
    # -cut-
    respond_to do |format|
      if @line_item.save
        format.html { redirect_to store_index_url, notice: 'Line item was successfully created.' }
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end
end

Commit!

Play around with your app and commit your progress.

Remote: True

So, after all the preparations, how do we actually add Ajax to our Rails app? First, we start by changing our "Add to Cart" button to call asynchronous request.

<p id="notice"><%= notice %></p>
<h1>Your Food Catalog</h1>

<% @foods.each do |food| %>
  <div class="entry">
    <%= image_tag(food.image_url) %> 
    <h3><%= food.name %></h3>
    <%= sanitize(food.description) %>
    <div class="price_line">
      <span class="price">
        <%= number_to_currency(food.price, unit: "Rp ", delimiter: ".", separator: ",") %>
        <%= button_to 'Add to Cart', line_items_path(food_id: food), remote: true %>
      </span>
    </div>
  </div>
<% end %>

Format JS

Next, we change our LineItems controller create method to also respond to JS format.

class LineItemsController < ApplicationController
  include CurrentCart
  before_action :set_cart, only: [:create]

  def create
    food = Food.find(params[:food_id])
    @line_item = @cart.add_food(food)

    respond_to do |format|
      if @line_item.save
        format.html { redirect_to store_index_url, notice: 'Line item was successfully created.' }
        format.js
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end
end

Adding JS File

Lastly, we add a javascript file in our views. Put this in "app/views/line_items/create.js.erb"

$('#cart').html("<%=j render(@cart) %>");

Try to add foods to cart in your app now.

What Happened?

  1. We tell our "button_to" to execute its request asynchronously by adding parameter "remote: true"
  2. Then, we add our view to respond_to Javascript by adding the line "format.js"
  3. With these two changes, when the button "Add to Cart" is clicked, Rails will look for "create" template that is written in javascript format
  4. We then provide this javascript file in the form of our "create.js.erb" file
  5. Our javascript file uses jQuery library (aliased with $) that replace html element with id "cart" with whatever rendered by our @cart object

Commit!

You have made a great progress so far. Let's commit your progress this far to your git repository.

Highlighting and Hiding

You have successfully moved your cart to the sidebar and updated it with Ajax. Now your product manager wants a fancier interaction in your cart. He wants your cart to:

  1. Highlighting changes in the cart whenever it happens
  2. Hiding the cart when it is empty

Are you up to this task?

Highlighting Changes

Let's start with highlighting changes in our cart.

jQuery UI (1)

We have utilized a little bit of jQuery in our Ajax cart. There are more things we can make use of from jQuery. For this, we need to install jQuery UI gem.

gem 'jquery-ui-rails'

Don't forget to run "bundle install" after that.

jQuery UI (2)

We need to include some jQuery effect in our application's javascript ("app/assets/javascripts/application.js") file.

//= require jquery
//= require jquery-ui/effects/effect-blind
//= require jquery_ujs

Don't forget to run restart your rails server after this.

Modifying LineItems Controller

Pass @current_item variable to our respond_to block that looks for javascript format.

class LineItemsController < ApplicationController
  # -cut-
  def create
    # -cut-
    respond_to do |format|
      if @line_item.save
        format.html { redirect_to store_index_url, notice: 'Line item was successfully created.' }
        format.js { @current_item = @line_item }
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end
end

Modifying LineItem Partial

Give <tr> tag an id "current_item" if it is the same item as the current_item variable that we pass from our LineItems controller.

<% if line_item == @current_item %>
  <tr id="current_item">
<% else %>
  <tr>
<% end %>
    <td><%= line_item.quantity %> ×</td>
    <td><%= line_item.food.name %></td>
    <td class="item_price"><%= number_to_currency(line_item.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
  </tr>

Modifying Javascript

Change our "create.js.erb" file to change some css attributes of html element with id "current_item".

$('#cart').html("<%=j render(@cart) %>");

$('#current_item').css({'background-color':'#88ff88'}).
  animate({'background-color':'#114411'}, 1000);

Try adding more foods to your cart and watch how nicely it is animated when you do that.

Hiding Empty Cart

There are many ways to hide our empty cart. First, let's try by modifying our view.

<% unless cart.line_items.empty? %>
  <table>
    <%= render (@cart.line_items) %>
    
    <tr class="total_line">
      <td colspan="2">Total</td>
      <td class="total_cell"><%= number_to_currency(@cart.total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
    </tr>
  </table>

  <%= button_to 'Empty cart', @cart, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>

Modifying Javascript

To make a smoother, we use jQuery library to add interface effect when the cart is created for the first time.

if ($('#cart tr').length == 1) { $('#cart').show('blind', 1000); }

$('#cart').html("<%=j render(@cart) %>");

$('#current_item').css({'background-color':'#88ff88'}).
  animate({'background-color':'#114411'}, 1000);

Other Options

The next option is by modifying css for cart in the application layout. You don't have to write this one.

<div id="cart"
  <% if @cart.line_items.empty? %>
    style="display: none"
  <% end %>
>

<%= render(@cart) %> </div>

The code above works just fine but also looks terrible, not just aesthetically. We can abstract some processing in our view using helper methods.

Helper

Instead of using if-else clause inline, we create a new helper named "hidden_div_if" like this:

module ApplicationHelper
  def hidden_div_if(condition, attributes = {}, &block)
    if condition
      attributes["style"] = "display: none"
    end
    content_tag("div", attributes, &block)
  end
end

Modifying Application Layout

And call it in our application layout like this:

<div id=cart>
  <% if @cart %>
    <%= hidden_div_if(@cart.line_items.empty?, id: 'cart') do %>
      <%= render @cart %>
    <% end %>
  <% end %>
</div>

Commit!

You have made a great progress so far. Let's commit your progress this far to your git repository.

The Simplified Go Food Web App - Iteration 3

By qblfrb

The Simplified Go Food Web App - Iteration 3

  • 329