Currently, our cart is rendered by theshowaction in theCartControllerand the corresponding .html.erbtemplate. What we’d like to do is to move that rendering into the sidebar. This means it will no longer be in its own page. Instead, we’ll render it in the layout that displays the overall catalog. And that’s easy using partial templates.
Partial Templates
Programming languages let you definemethods. A method is a chunk of code with a name: invoke the method by name, and the corresponding chunk of code gets run. And, of course, you can pass parameters to a method, which lets you write one piece of code that can be used in many different circumstances.
You can think of Rails partial templates (partialsfor short) as a kind of method for views. A partial is simply a chunk of a view in its own separate file. You can invoke (render) a partial from another template or from a controller, and the partial will render itself and return the results of that rendering. And, just as with methods, you can pass parameters to a partial, so the same partial can render different results.
We’ll use partials twice in this iteration. First, let’s look at the cart display itself:
Download depot_i/app/views/carts/show.html.erb
<div class="cart_title">Your Cart</div>
<table>
<% @cart.line_items.each do |item| %>
<tr>
<td><%= item.quantity %>×</td>
<td><%= item.product.title %></td>
<td class="item_price"><%= number_to_currency(item.total_price) %></td>
</tr>
<% end %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(@cart.total_price) %></td>
</tr>
</table>
<%= button_to 'Empty cart', @cart, :method => :delete, :confirm => 'Are you sure?' %>
It creates a list of table rows, one for each item in the cart. Whenever you find yourself iterating like this, you might want to stop and ask yourself, is this too much logic in a template? It turns out we can abstract away the loop using
ITERATIONF1: MOVING THECAR T 141 do this, we’ll make use of the fact that you can pass a collection to the method
that renders partial templates, and that method will automatically invoke the partial once for each item in the collection. Let’s rewrite our cart view to use this feature:
Download depot_j/app/views/carts/show.html.erb
<div class="cart_title">Your Cart</div>
<table>
<%= render(@cart.line_items) %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(@cart.total_price) %></td>
</tr>
</table>
<%= button_to 'Empty cart', @cart, :method => :delete, :confirm => 'Are you sure?' %>
That’s a lot simpler. The render method will iterate over any collection that is passed to it. The partial template itself is simply another template file (by default in the same directory as the object being rendered and with the name of the table as the name). However, to keep the names of partials distinct from regular templates, Rails automatically prepends an underscore to the partial name when looking for the file. That means we need to name our partial _line_item.html.erband place it in theapp/views/line_itemsdirectory.
Download depot_j/app/views/line_items/_line_item.html.erb
<tr>
<td><%= line_item.quantity %>×</td>
<td><%= line_item.product.title %></td>
<td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
</tr>
There’s something subtle going on here. Inside the partial template, we refer to the current object using the variable name that matches the name of the template. In this case, the partial is namedline_item, so inside the partial we expect to have a variable calledline_item.
So, now we’ve tidied up the cart display, but that hasn’t moved it into the side- bar. To do that, let’s revisit our layout. If we had a partial template that could display the cart, we could simply embed a call like this within the sidebar:
render("cart")
But how would the partial know where to find the cart object? One way would be for it to make an assumption. In the layout, we have access to the @cart instance variable that was set by the controller. It turns out that this is also
Report erratum this copy is(P1.0 printing, March 2011)
Download from Wow! eBook <www.wowebook.com>
ITERATIONF1: MOVING THECAR T 142 available inside partials called from the layout. However, this is a bit like call-
ing a method and passing it some value in a global variable. It works, but it’s ugly coding, and it increases coupling (which in turn makes your programs brittle and hard to maintain).
Now that we have a partial for a line item, let’s do the same for the cart. First, we’ll create the_cart.html.erb template. This is basically ourcarts/show.html.erb template but using cart instead of @cart. (Note that it’s OK for a partial to invoke other partials.)
Download depot_j/app/views/carts/_cart.html.erb
<div class="cart_title">Your Cart</div>
<table>
<%= render(cart.line_items) %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
</tr>
</table>
<%= button_to 'Empty cart', cart, :method => :delete, :confirm => 'Are you sure?' %>
As the Rails mantra goes, Don’t Repeat Yourselves (DRY), and we have just done that. At the moment the two files are in sync, so there may not seem to be much of a problem, but having one set of logic for the Ajax calls and another set of logic to handle the case where JavaScript is disabled invites problems.
Let’s avoid all of that and replace the original template with code that causes the partial to be rendered:
Download depot_k/app/views/carts/show.html.erb
<%= render @cart %>
Now we will change the application layout to include this new partial in the sidebar:
Download depot_j/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "scaffold" %>
<%= stylesheet_link_tag "depot", :media => "all" %>
<%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>
</head>
ITERATIONF1: MOVING THECAR T 143
<body id="store">
<div id="banner">
<%= image_tag("logo.png") %>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<div id="cart">
<%= render @cart %>
</div>
<a href="http://www....">Home</a><br />
<a href="http://www..../faq">Questions</a><br />
<a href="http://www..../news">News</a><br />
<a href="http://www..../contact">Contact</a><br />
</div>
<div id="main">
<%= yield %>
</div>
</div>
</body>
</html>
Now we have to make a small change to the store controller. We’re invoking the layout while looking at the store’s index action, and that action doesn’t currently set@cart. That’s easy enough to remedy:
Download depot_j/app/controllers/store_controller.rb
def index
@products = Product.all
@cart = current_cart end
Now we add a bit of CSS:
Download depot_j/public/stylesheets/depot.css
/* Styles for the cart in the sidebar */
#cart, #cart table { font-size: smaller;
color: white;
}
#cart table {
border-top: 1px dotted #595;
border-bottom: 1px dotted #595;
margin-bottom: 10px;
}
If you display the catalog after adding something to your cart, you should see something like Figure 11.1, on the next page. Let’s just wait for the Webby Award nomination.
Report erratum this copy is(P1.0 printing, March 2011)
Download from Wow! eBook <www.wowebook.com>
ITERATIONF1: MOVING THECAR T 144
Figure 11.1: The cart is in the sidebar.
Changing the Flow
Now that we’re displaying the cart in the sidebar, we can change the way that the Add to Cart button works. Rather than displaying a separate cart page, all it has to do is refresh the main index page.
The change is pretty simple: at the end of thecreateaction, we simply redirect the browser back to the index:
Download depot_k/app/controllers/line_items_controller.rb
def create
@cart = current_cart
product = Product.find(params[:product_id])
@line_item = @cart.add_product(product.id) respond_to do |format|
if @line_item.save
format.html { redirect_to(store_url) } format.xml { render :xml => @line_item,
:status => :created, :location => @line_item } else
format.html { render :action => "new" }
format.xml { render :xml => @line_item.errors, :status => :unprocessable_entity }
end end
ITERATIONF2: CREATING ANAJAX-BASEDCAR T 145 So, now we have a store with a cart in the sidebar. When we click to add an
item to the cart, the page is redisplayed with an updated cart. However, if our catalog is large, that redisplay might take a while. It uses bandwidth, and it uses server resources. Fortunately, we can use Ajax to make this better.