Chapter 11. Task F. Add a Dash of Ajax
6.1 使用partial重构Cart:
??? partial(局部)模板简称:partials.可以在其他的模板或Controller中调用(呈现)这个局部模板.
??? 那么在调用的地方,局部模板就可以呈现出数据.
??? 你也可以传递参数给局部模板就像传递参数给方法一样来得到不同的呈现结果(传递了Collection).
???
??? 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).
??? 局部模板默认所在的视图目录是 和模板里要呈现的对象(line_item)的相同目录下(views/line_items/).
??? 局部模板的文件名默认是render括号里的(最终)参数,如果是对象关联,则采用单数形式.???
???
??? Inside the partial template, we refer to the current object using the variable name that
??? matches the name of the template.? _line_item.html.erb对应的模板为line_item(:去掉文件名头的_)
??? 所以在局部模板里,使用和模板名字相同的变量即line_item.
-> app/views/carts/show.html.erb <% @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 %> 替换为: <%= render(@cart.line_items) %> -> 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>
6.2 继续重构partial,传递实例变量给render:?? ?
?? ?move the cart into the sidebar.If we had a partial template that could display the cart,
?? ?we could simply embed a call like this within the sidebar:? render("cart")
?? ?
?? ?In the layout(views/carts/show.html.erb), we have access to the @cart instance variable
?? ?that was set by the controller. And this is also available inside partials called from the layout.
?? ?在show.html.erb这个Layout中可以使用在Controller中设置的实例变量@cart.在partials中同样可以使用实例变量.
?? ?
?? ?既然render可以跟上Collection参数:render(@cart.line_items) 当然也可以只跟上参数render @cart.
?? ?不同的是传递@cart.line_items参数,在partial(_line_item)中不需要再循环.
?? ?而传递@cart参数,则在partial(_cart)中需要根据cart取得对应的line_items.
??? 和_line_item.html.erb中使用变量line_item一样.这里_cart.html.erb也是使用cart变量.
-> app/views/carts/show.html.erb <%= render @cart %> views/carts/show.html.erb只有一行代码. render跟上的参数是实例变量@cart -> 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?' %>
6.3 把Cart购物车信息呈现在sidebar:??? ???
??? 局部模板可以在任何地方引用,所以如果要在主页面(localhost:3000)的sidebar上显示局部模板的内容
??? 同时要在对应的Controller(StoreController)中定义对应的实例变量(@cart).
???
??? 正如在carts/show.html.erb中能够使用@cart是因为line_items_controller.rb的create方法中定义了@cart变量
??? 在redirect_to @line_item.cart即carts/show...中就能使用该变量(为什么重定向到show见前面的分析).
??? 改变控制流:之前是点击Product的购买按钮(即每创建一个LineItem到购物车中),就转到购物车页面(carts/show).
??? 如果这时返回到localhost:3000 就会在主页面的sidebar上同样能看到购物车信息.
??? 而我们想要在点击购买时不跳转到购物车页面, 而是直接在原来的页面的sidebar上显示购物车信息.
-> app/views/layouts/application.html.erb <div id="cart"> <%= render @cart %> </div> -> app/controllers/store_controller.rb class StoreController < ApplicationController def index @products = Product.all @cart = current_cart end end -> app/controllers/line_items_controller.rb if @line_item.save #format.html{ redirect_to @line_item.cart} format.html { redirect_to(store_url) }
??? When we click to add an item to the cart, the page is redisplayed with an updated cart.
?? ?重定向到主页面是整个页面都刷新,而变化的只是partial中的内容.其他内容一般是不会变化的.Ajax Coming!
?? ?
6.4 购买按钮发送Ajax请求,应用程序使用RJS模板呈现内容:?? ?
?? ?change the catalog page to send an Ajax to our server application
?? ?button_to:create the link to the create action.加上:remote参数表示现在发送的是一个Ajax请求.
?? ?改变页面向服务器端应用程序发起一个Ajax请求,
?? ?
?? ?have the application respond with the HTML fragment containing the updated cart.
?? ?create the updated HTML fragment that represents the cart
?? ?to have the browser stick that HTML into the browser’s internal representation of DOM being displayed.
?? ?应用程序响应,并应答一段HTML代码(展示了最新的购物车).
?? ?创建一段HTML代码来代表购物,然后让浏览器把这段代码插入到当前页面的DOM,替换掉当前页面显示的购物车.
?? ?
?? ?when create finishes handling the Ajax request, Rails will look for a create template to render
?? ?当在create方法中处理完Ajax请求(点击购买,调用create方法,line_item.save成功)
?? ?根据format.js,Rails会去寻找对应的模板来呈现内容:create.js.rjs.
?? ?正如format.html(后面没有加上redirect_to).默认对应的是方法名.文件后缀.比如new方法:format.html->new.html.erb
?? ?
?? ?A .js.rjs template is a way of getting JavaScript on the browser to do what you want,
?? ?all by writing server-side Ruby code. rjs模板允许你能够在浏览器上得到JS代码即:将JavaScript发送到浏览器.
-> app/views/store/index.html.erb <%= button_to 'Add to Cart', line_items_path(:product_id => product), :remote => true %> -> app/controllers/line_items_controller.rb def create respond_to do |format| if @line_item.save format.html { redirect_to(store_url) } format.js end end end -> app/views/line_items/create.js.rjs page.replace_html('cart', render(@cart))
??? The page variable is an instance of JavaScript generator-a Rails class that knows
?? ?how to create JavaScript on the server and have it executed by the browser.
?? ?Rails提供的JavaScript(帮助类,application Layout中包含库:<%= javascript_include_tag :defaults %>)
?? ?生成器类的实例变量page,能够在服务器端创建JS,并使其在浏览器上运行.
?? ?replace the content of the element on the current page with the id cart with the rendered partial for a given cart.
?? ?This simple RJS template then tells the browser to replace the content of the element whose id="cart" with that HTML.
?? ?将当前页面id为cart的元素替换成render @cart的内容(局部模板里的内容就是最新的购物车的内容).
?? ?调试Ajax程序:
?? ?查看development.log是否有报错;或者是否有请求调用到create方法.若没有说明浏览器没有发起Ajax请求.
?? ?查看页面源文件检查JS库是否加载到浏览器;?? 浏览器缓存;? 针对IE的头信息:<!DOCTYPE html>
?? ?
6.5 高亮显示最近更新的购物项:
?? ?identifying the most recently updated item in the cart.
?? ?pass the current line item down to the template(create.js.rjs) by assigning it to an instance variable
?? ?标识购物车中最近更新的购物项. Ajax请求处理完后(在format.js后添加)将当前购物项作为实例变量返回.
?? ?
?? ?In the _line_item.html.erb partial, we then check to see whether the item we’re rendering is
?? ?the one that just changed. If so, we tag it with an id of current_item:
?? ?在购物项局部模板中(因为最终显示的购物项内容其实是在_line_item局部模板里)而不是在application或者carts/show中.
?? ?在Controller的Action#create(添加完购物项保存成功)设置的实例变量,在局部模板里仍然是可以访问到的@current_item.
?? ?在_cart局部模板里render cart.line_items循环购物的每个购物项,如果当前循环到的购物项=实例变量里保存的变量
?? ?@current_item 则把这个购物项标记一个id:<tr id="current_item">.最后只要把黄渐变的效果运用到这个元素上即可.
?? ?
?? ?now the <tr> element of the most recently changed item in the cart will be tagged with id="current_item".?? ?
?? ?identified the browser element that apply the effect to by passing :current_item to the page.
-> app/controllers/line_items_controller.rb def create if @line_item.save format.html { redirect_to(store_url) } format.js { @current_item = @line_item } -> app/views/line_items/_line_item.html.erb <% if line_item == @current_item %> <tr id="current_item"> <% else %> <tr> <% end %> <td><%= line_item.quantity %>×</td> <td><%= line_item.product.title %></td> <td class="item_price"><%= number_to_currency(line_item.total_price) %></td> </tr> -> app/views/line_items/create.js.rjs page[:current_item].visual_effect :highlight, :startcolor => "#88ff88", :endcolor => "#114411"
6.6 隐藏空购物项:
??? 当购物车为空时,<div id="cart" style="display:none">不显示购物车.
?
-> app/views/line_items/create.js.rjs #采用Ajax将最近更新的购物项替换原来的购物车 page.replace_html('cart', render(@cart)) #如果购物车中有了一项购物项,则在边框就应该显示购物车. page[:cart].visual_effect :blind_down if @cart.total_items == 1 #高亮显示最近更新的购物项 page[:current_item].visual_effect :highlight, :startcolor => "#88ff88", :endcolor => "#114411" -> app/models/cart.rb #6种写法: 成员变量前@ 数组.to_a 用Symbol:代替临时变量 def total_price line_items.to_a.sum {|item| item.total_price} #@line_items.to_a.sum {|item| item.total_price} #@line_items.sum {|item| item.total_price} #line_items.sum {|item| item.total_price} #@line_items.sum(:total_price) #line_items.sum(:total_price) end def total_items #购物车所有购物项的所有数量.比如购物项1 数量2 购物项2 数量3 则只为5 line_items.sum(:quantity) #line_items.sum{ |item| item.quantity } end -> app/views/layouts/application.html.erb <div id="cart" <% if @cart.line_items.empty? %>style="display:none"<% end %> > <%= render(@cart) %> </div>
??? using Ajax to add products to the cart, the main page doesn’t get redrawn
?? ?between requests as people shop.? we’ll continue to display the flash message
?? ?saying the cart is empty even as we display a cart in the sidebar.
?? ?清空购物车时,不需要显示flash消息. 否则如果再次往购物车中添加Product,因为采用了Ajax
?? ?只刷新了局部模板,没有刷新整个页面,导致flash消息仍然停留在主页面上,造成困惑.
-> app/controllers/carts_controller.rb def destroy #format.html { redirect_to(store_url, :notice => 'Your cart is currently empty') } format.html { redirect_to(store_url) }
?? abstract some processing out of a view (any kind of view),write a helper method.
?? ?当需要将处理逻辑从视图(partial,rjs模板)中抽象出来,可以编写辅助方法.
?? ?
6.7 抽象视图中的处理逻辑:
?? ?It take a condition, an optional set of attributes, and a block.
?? ?if the condition is true,adding the display: none style.
?? ?It wraps the output(content_tag) generated by the block in a <div> tag. [wraps output]
?? ?把(传递给它:content_tag的)代码块的输出用标记(div)包装起来.如条件为真,则(div)加上style样式
?? ?
?? ?By using the &block notation, we get Ruby to pass the block that was given to hidden_div_if
?? ?down to content_tag.? [pass block down to content_tag]
?? ?通过&block标志,将代码块经过hidden_div_if()传递给content_tag(). 有点类似回调函数.参数继续传递.
-> app/helpers/application_helper.rb module ApplicationHelper def hidden_div_if(condition, attributes = {}, &block) if condition attributes["style"] = "display: none" end content_tag("div", attributes, &block) end end -> app/views/layouts/application.html.erb <%= hidden_div_if(@cart.line_items.empty?, :id => "cart") do %> <%= render @cart %> <% end %>
??? make the cart hide and reveal itself
?? ?by making the CSS display style conditional on the number of items in the cart.? ?
?? ?used an RJS template to invoke the blind_down effect when the cart went from being empty to having one item.
?? ?显示和隐藏购物车: 根据Cart中购物项的数量设置CSS样式;当第一件Product被放进购物车时,通过RJS模板调用效果.
?? ?
6.8 测试采用Ajax引起的问题:
?? ?> rake test
?? ?或者访问http://localhost:3000/products报错:NoMethodError in Products#index
?? ?where line #17 raised: <%= hidden_div_if(@cart.line_items.empty?, :id => "cart") do %>
?? ?
?? ?如果把store_controller.rb的@cart = current_cart注释掉.同样报错:NoMethodError in Store#index
?? ?where line #17 raised:<%= hidden_div_if(@cart.line_items.empty?, :id => "cart") do %>
?? ?这就是我们前面说到的.如果Controller中没有定义变量@cart,在视图中使用就无法使用相应的变量(6.3).
?? ?@cart is apparently nil when we display an index of our products.it is set only in the store controller.
?? ?avoid displaying the cart at all unless this value is set:
-> app/views/layouts/application.html.erb <% if @cart %> <%= hidden_div_if(@cart.line_items.empty?, :id => "cart") do %> <%= render @cart %> <% end %> <% end %> -> test/functional/line_items_controller_test.rb test "should create line_item" do assert_difference('LineItem.count') do #post :create, line_item: @line_item.attributes post :create, :product_id => products(:ruby).id end #assert_redirected_to line_item_path(assigns(:line_item)) #assert_redirected_to cart_path(assigns(:line_item).cart) assert_redirected_to store_path end test "should create line_item via ajax" do assert_difference('LineItem.count') do xhr :post, :create, :product_id => products(:ruby).id end #Instead of a redirect,we expect a successful response assert_response :success #containing a call to replace the HTML for the cart #extract the relevant HTML and then processing that HTML via whatever additional assertions you want to apply assert_select_rjs :replace_html, 'cart' do #find a row with an id of current_item with a value matching Programming Ruby 1.9 assert_select 'tr#current_item td', /Programming Ruby 1.9/ end end?
?