• 沒有找到結果。

Iteration F1: Moving the Cart

在文檔中 Prepared exclusively for Jared Rosoff (頁 154-159)

Task F: Add a Dash of Ajax

11.1 Iteration F1: Moving the Cart

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 define methods. 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 (partials for 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>

<% for item in @cart.line_items %>

<tr>

<%= 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 partials (and, as we’ll see, this also sets the stage for some Ajax magic later). To

ITERATIONF1: MOVING THECAR T 155

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:

<%= 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 that we need to name our partial _line_item.html.erband place it theapp/views/line_itemsdirectory.

Download depot_j/app/views/line_items/_line_item.html.erb

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(:partial => "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 available inside partials called from the layout. However, this is a bit like

call-Report erratum this copy is (B8.0 printing, September 9, 2010)

Prepared exclusively for Jared Rosoff

ITERATIONF1: MOVING THECAR T 156

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.)

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

Now we’ll change the application layout to include this new partial in the side-bar:

<%= stylesheet_link_tag "depot" , :media => "all" %>

<%= javascript_include_tag :defaults %>

<%= @page_title || "Pragmatic Bookshelf" %>

</div>

ITERATIONF1: MOVING THECAR T 157

</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 Figure11.1, on the following page. 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 displaying a separate cart page, all it has to do is refresh the main index page.

Report erratum this copy is (B8.0 printing, September 9, 2010)

Prepared exclusively for Jared Rosoff

ITERATIONF1: MOVING THECAR T 158

Figure 11.1: The cart is in the sidebar.

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 end

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.

ITERATIONF2: CREATING ANAJAX-BASEDCAR T 159

在文檔中 Prepared exclusively for Jared Rosoff (頁 154-159)