Yesterday we have:
We have written features to add foods to cart. But what about removing them from it? Let's start with the requirements:
Based on requirements above, modify Carts controller spec for "destroy" method!
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
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
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
Now try to modify your cart's "destroy" method to make it pass.
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
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?' } %>
Play around with your app and commit your progress.
Asynchronous Javascript and XML
Ajax is a combination of:
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.
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:
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?' } %>
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".
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?
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?' } %>
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>
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;
}
}
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 */
}
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!
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
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!
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
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
Play around with your app and commit your progress.
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 %>
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
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.
You have made a great progress so far. Let's commit your progress this far to your git repository.
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:
Are you up to this task?
Let's start with highlighting changes in our cart.
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.
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.
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
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>
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.
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 %>
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);
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.
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
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>
You have made a great progress so far. Let's commit your progress this far to your git repository.