Iteration F1: Moving the Cart

Currently, our cart is rendered by the show action in the CartController and the corresponding html.erb template. We’d like to move that rendering into the sidebar. This means it’ll no longer be in its own page. Instead, we’ll render it in the layout that displays the overall catalog. You can do that using partial templates.

Partial Templates

Programming languages let you define methods. A method is a chunk of code with a name: invoke the method by the name, and the corresponding chunk of code gets run. And, of course, you can pass parameters to a method, which lets you write a piece of code that can be used in many different circumstances.

Think of Rails partial templates (partials for short) like a method for views. A partial is simply a chunk of a view in its own separate file. You can invoke (aka render) a partial from another template or from a controller, and the partial will render itself and return the results of that rendering. 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:

 <article>
 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
  <h2>Your Cart</h2>
  <table>
 <%​ @cart.​line_items​.​each​ ​do​ |line_item| ​%>
  <tr>
  <td class=​"quantity"​>​<%=​ line_item.​quantity​ ​%>​</td>
  <td>​<%=​ line_item.​product​.​title​ ​%>​</td>
  <td class=​"price"​>​<%=​ number_to_currency(line_item.​total_price​) ​%>​</td>
  </tr>
 <%​ ​end​ ​%>
  <tfoot>
  <tr>
  <th colspan=​"2"​>Total:</th>
  <td class=​"price"​>​<%=​ number_to_currency(@cart.​total_price​) ​%>​</td>
  </tr>
  </tfoot>
  </table>
 <%=​ button_to ​'Empty cart'​, @cart,
 method: :delete​,
 data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>
 
 </article>

It creates a list of table rows, one for each item in the cart. Whenever you find yourself iterating like this, you should stop and ask yourself, is this too much logic in a template? It turns out we can abstract away the loop by using partials (and, as you’ll see, this also sets the stage for some Ajax later). To do this, 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:

 <article>
 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
  <h2>Your Cart</h2>
  <table>
 
»<%=​ render(@cart.​line_items​) ​%>
  <tfoot>
  <tr>
  <th colspan=​"2"​>Total:</th>
  <td class=​"price"​>​<%=​ number_to_currency(@cart.​total_price​) ​%>​</td>
  </tr>
  </tfoot>
  </table>
 <%=​ button_to ​'Empty cart'​, @cart,
 method: :delete​,
 data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>
 
 </article>

That’s a lot simpler. The render method will iterate over any collection that’s passed to it. The partial template 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.erb and place it in the app/views/line_items directory:

 <tr>
  <td class=​"quantity"​>​<%=​ line_item.​quantity​ ​%>​</td>
  <td>​<%=​ line_item.​product​.​title​ ​%>​</td>
  <td class=​"price"​>​<%=​ number_to_currency(line_item.​total_price​) ​%>​</td>
 </tr>

Something subtle is going on here. Inside the partial template, we refer to the current object by using the variable name that matches the name of the template. In this case, the partial is named line_item, so inside the partial we expect to have a variable called line_item.

So now we’ve tidied up the cart display, but that hasn’t moved it into the sidebar. To do that, let’s revisit our layout. If we had a partial template that could display the cart, we could embed a call like this within the sidebar:

 render(​"cart"​)

But how would the partial know where to find the cart object? One way is for it to make an assumption. In the layout, we have access to the @cart instance variable that was set by the controller. Turns out that this is also available inside partials called from the layout. But this is like calling a method and passing it a value in a global variable. It works, but it’s ugly coding, and it increases coupling (which in turn makes your programs 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 our carts/show.html.erb template but using cart instead of @cart (note that it’s OK for a partial to invoke other partials).

 <article>
 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
  <h2>Your Cart</h2>
  <table>
 
»<%=​ render(cart.​line_items​) ​%>
  <tfoot>
  <tr>
  <th colspan=​"2"​>Total:</th>
» <td class=​"price"​>​<%=​ number_to_currency(cart.​total_price​) ​%>​</td>
  </tr>
  </tfoot>
  </table>
»<%=​ button_to ​'Empty cart'​, cart,
 method: :delete​,
 data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>
 
 </article>

As the Rails mantra goes, don’t repeat yourself (DRY). But we’ve 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:

»<%=​ render @cart ​%>

Now change the application layout to include this new partial in the sidebar:

 <!DOCTYPE html>
 <html>
  <head>
  <title>Pragprog Books Online Store</title>
 <%=​ csrf_meta_tags ​%>
 
 <%=​ stylesheet_link_tag ​'application'​, ​media: ​​'all'​,
 'data-turbolinks-track'​: ​'reload'​ ​%>
 <%=​ javascript_pack_tag ​'application'​,
 'data-turbolinks-track'​: ​'reload'​ ​%>
  </head>
 
  <body>
  <header class=​"main"​>
 <%=​ image_tag ​'logo.svg'​, ​alt: ​​'The Pragmatic Bookshelf'​ ​%>
  <h1>​<%=​ @page_title ​%>​</h1>
  </header>
  <section class=​"content"​>
  <nav class=​"side_nav"​>
» <div id=​"cart"​ class=​"carts"​>
»<%=​ render @cart ​%>
» </div>
  <ul>
  <li><a href=​"/"​>Home</a></li>
  <li><a href=​"/questions"​>Questions</a></li>
  <li><a href=​"/news"​>News</a></li>
  <li><a href=​"/contact"​>Contact</a></li>
  </ul>
  </nav>
  <main class=​'​​<%=​ controller.​controller_name​ ​%>​​'​>
 <%=​ ​yield​ ​%>
  </main>
  </section>
  </body>
 </html>

Note that we’ve given the <article> element that wraps the cart the CSS class carts. This will allow it to pick up the styling we added in Iteration E3: Finishing the Cart.

Next. 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 a quick change:

 class​ StoreController < ApplicationController
»include​ CurrentCart
» before_action ​:set_cart
 def​ ​index
  @products = Product.​order​(​:title​)
 end
 end

The data for the cart is common no matter where it’s placed in the output, but there’s no requirement that the presentation be identical independently of where this content is placed. In fact, black lettering on a green background is hard to read, so let’s provide additional rules for this table when it appears in the sidebar:

 #cart​ {
  article {
  h2 {
  margin-top: 0;
  }
  background: white;
  border-radius: 0.5em;
  margin: 1em;
  padding: 1.414em;
 @media​ (min-width: 30em) {
  margin: 0; ​// desktop doesn't need this margin
  }
  }
 }

If you display the catalog after adding something to your cart, you should see something like the screenshot.

images/j_1_side_cart.png

Let’s just wait for the Webby Award nomination.

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 display a separate cart page, all it has to do is refresh the main index page.

The change is straightforward. At the end of the create action, we redirect the browser back to the index:

 def​ ​create
  product = Product.​find​(params[​:product_id​])
  @line_item = @cart.​add_product​(product)
 
  respond_to ​do​ |format|
 if​ @line_item.​save
» format.​html​ { redirect_to store_index_url }
  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

At this point, we rerun our tests and see a number of failures:

 $ ​​bin/rails​​ ​​test
 Run options: --seed 57801
 
 # Running:
 
 ...​​E
 
 Error:
 ProductsControllerTest#​​test_should_show_product:
 ActionView::Template::Error: 'nil' is not an ActiveModel-compatible
 object. It must implement :to_partial_path.
 app/views/layouts/application.html.erb:21:in
 `_app_views_layouts_application_html_erb`

If we try to display the products index by visiting http://localhost:3000/products in the browser, we see the error shown in the following screenshot.

images/k_1_products_page_broken.png

This information is helpful. The message identifies the template file that was being processed at the point where the error occurs (app/views/layouts/application.html.erb), the line number where the error occurred, and an excerpt from the template of lines around the error. From this, we see that the expression being evaluated at the point of error is @cart.line_items, and the message produced is ’nil’ is not an ActiveModel-compatible object.

So, @cart is apparently nil when we display an index of our products. That makes sense, because it’s set only in the store controller. We can even verify this using the web console provided at the bottom of the web page. Now that we know what the problem is, the fix is to avoid displaying the cart at all unless the value is set:

 <nav class=​"side_nav"​>
»<%​ ​if​ @cart ​%>
»
» <div id=​"cart"​ class=​"carts"​>
»<%=​ render @cart ​%>
» </div>
»<%​ ​end​ ​%>
 
  <ul>
  <li><a href=​"/"​>Home</a></li>
  <li><a href=​"/questions"​>Questions</a></li>
  <li><a href=​"/news"​>News</a></li>
  <li><a href=​"/contact"​>Contact</a></li>
  </ul>
 </nav>

With this change in place, our tests now pass once again. Imagine what could have happened. A change in one part of an application made to support a new requirement breaks a function implemented in another part of the application. If you are not careful, this can happen in a small application like Depot. Even if you are careful, this will happen in a large application.

Keeping tests up-to-date is an important part of maintaining your application. Rails makes this as easy as possible to do. Agile programmers make testing an integral part of their development efforts. Many even go so far as to write their tests first, before the first line of code is written.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.200.66