`

Flv尝试:演示ajax在RJS中实现puff效果

阅读更多
真麻烦啊,最后还是搞定了



一些RJS消息

介绍
啥是 RJS ?

啥是 RJS ?Ajax 使用户与浏览器之间的交互体验更像是桌面程序。因为 Ajax 允许浏览器在后台发起远程调用。这些请求可以在无刷新的情况下更新当前页面。Ruby on Rails 框架内置对 Ajax 有极好的支持。Rails 1.0 支持基于 Ajax 的远程 JavaScript (RJS) 模板,将来,还允许你更新多个页面的元素。

RJS 模板是 Rails 1.1 增加的一个强大的功能。不同于其他的 Rails 那些需要渲染然后发送给浏览器的模板,RJS 模板用来更新那些已经被渲染过的页面。

Rails 1.1 之前,已经允许通过 Ajax 调用来更新单一页面的元素。可是,当你想更新多个页面的元素却很难。RJS 允许你在一个 Ajax 调用中使用 Ruby 代码更新多个页面的元素. 你可以在一个模板中使用许多 Script.aculo.us 提供的视觉效果,并且你根本不需要写任何 JavaScript。在大部分案例中,不再需要在 JavaScript 转换内容和程序。所有的 JavaScript 都由 Rails JavaScript Generator 产生, Ajax 响应由 prototype 库自动得到。

如你所知,RJS 让更新多个页面的元素和通过一个 Ajax 调用产生多个页面效果变的更简单。
第二章
从一个简单的应用开始

下面开始一个简单的介绍性的例子。我把这个应用命名为 "Thought Log"。 Thought Log 简单的获取文本框输入的数据并在无刷新的情况下把他记入当前页面。首先创建这个应用。
cody> rails thought_log

现在新建一个 controller,ThoughtsController,用来保存我们的 actions。
cody> ruby script/generate controller Thoughts
   exists app/controllers/
   exists app/helpers/
   create app/views/thoughts
   create test/functional/
   create app/controllers/thoughts_controller.rb
   create test/functional/thoughts_controller_test.rb
   create app/helpers/thoughts_helper.rb


发生器创建了 controller,helper 并为 controller 的视图创建了一个文件夹。现在我们增加两个 action。第一个 action 名为 index(), 显示最初的空的 thoughts 的列表。第二个 action 名为 log(),在后台调用 Ajax 并将新的 thought 增加到当前页面。
class ThoughtsController < ApplicationController
 def index
 end

 def log
  @thought = params[:thought]
 end
end


log() action 简单的将值 params[:thought] 赋给实例变量 @thought 然后渲染 app/views/thoughts/log.rjs 模板。我们根据 Rails 的约定, controller 默认渲染和 action 同名的模板。既然 .rjs 模板也是另一个模板,Rails 在 controller 的视图文件夹去寻找 log.rjs 模板。唯一的缺点是在视图文件夹下有同名的模板,Rails 会在渲染 .rjs 模板之前先渲染 .rhtml 和 .rxml。在 Rails 下相当于:
def log
end

def log
 render :action => 'log'
end


其实你不必声明 index() action,Rails 默认就会调用 index() action 并渲染该控制器视图文件夹下的 index.rhtml 模板文件。
NOTE

RJS 模板依靠 prototype.js JavaScript 库。如果你希望使用 Script.aculo.us 视觉效果和控制器,你也需要 effects.js,controls.js 和 dragdrop.js。你可以在项目中在视图中增加 javascript_include_tag :defaults。这个也会把 application.js 文件载入进来。

接下来我们创建最初的视图。这个页面创建远程 Ajax 调用。通常你要为应用创建一个布局,但是因为这个例子是只有一个页面,我们把一切都放到视图模板中。在 app/views/thoughts 文件夹中创建 index.rhtml。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <title>Thought Log</title>
  <%= javascript_include_tag :defaults %>
 </head>
 <body>
  <h1>My thoughts</h1>

  <% form_remote_tag :url => { :action => 'log' }, :html => { :id => 'thought-form' } do %>
  <%= text_field_tag 'thought', nil, :size => 40 %>
  <%= submit_tag 'Log thought' %>
  <%= end %>

  <div id="thoughts"></div>
 </body>
</html>


使用 javascript_include_tag 这个辅助方法,来包含 Rails 中所需要的 JavaScript 库是很简单的。辅助方法后面跟随符号 :defaults 会调用 Rails 中的全部 JavaScript 库,包括你自己编写的 application.js,如果它存在的话。

在引入必须的 JavaScript 文件后, 我们创建一个仅含有一个文本输入框的简单的表单。我们不使用 form_tag() 辅助方法,而使用 form_remote_tag() 辅助方法。两者不同的是 form_remote_tag() 在后台使用 Ajax 来更新表单和提交数据,而不是像 form_tag() 重新渲染页面。参数 :url 指定控制器中收取表单数据的 action。这里,我们把数据提交给当前控制器 ThoughtsController 中的 log() action。我们通过 :html 选项来给表单增加一个 id。 这允许我们通过 RJS 模板在完成提交动作后重置表单。

最后,视图有一个 id 是 thoughts 的空的 <div> 元素。id 让我们可以通过 RJS 模板来更新页面元素。thoughts <div> 是一个容器用来包含表单提交的数据。接下来,我们终于要创建实际的 RJS 模板,你可以看看一切是怎么结合起来的。

现在在做主要的视图之前,我们需要一个小的局部模板来绘制 logged thoughts。局部模板让 RJS 模板保持干净和简单。创建局部模板 app/views/thoughts/_thought.rhtml 并填入下面的代码。
<p>
 <span style="font-size: 0.8em;">[<%= Time.now.to_s(:db) %>]</span>
 <%=h thought %>
</p>


这个局部模板很简单。它只是在一个段落中显示当前时间和 logged thought。这里有一个很棒的习惯做法,使用 h() 来格式化用户输入的文本,避免页面执行其中一些不需要的脚本。
NOTE

在使用 RJS 模板执行远程调用的时候绝对不要使用 :update 选项。 :update 选项通知 Rails 产生一个 Ajax.Updater Prototype 对象 而不是 Ajax.Request 对象。Ajax.Updater 使用返回的 HTML 来更新单一的 DOM 元素。RJS 模板返回 JavaScript 给浏览器,来产生我们希望得到的效果。

最后,创建 RJS 模板,我们遵守 Rails 约定,把视图的名字和 action 的保持一致。这样我们就不需要在控制器中明确的声明 render()。创建 app/views/thoughts/log.rjs 并填入随后的代码。
page.insert_html :bottom, 'thoughts', :partial => 'thought'
page.visual_effect :highlight, 'thoughts'
page.form.reset 'thought-form'


首先,这个 page 对象从哪来滴?page 对象实际上是一个 Rails 的 JavaScriptGenerator 实例,它产生全部 JavaScript 并传递给浏览器。所有的 RJS 方法都在这个page 对象上调用。下一章我们将仔细分析 RJS 模板在 Rails 中的处理方法。

既然我们在后台已经有了少量的 page 对象,我们可以考虑模板中的个别调用。首先,局部模板 app/views/thoughts/_thought.rhtml 会被渲染,产生的结果插入 thoughts <div> 的最下面。第一个参数是插入新内容的位置,我们可以有4个选项,包括 :before, :after, :bottom 和 :top。 第二个参数指定 DOM 元素的 id,内容插入其中。第三个参数可以是字符串或hash参数来调用 ActionView#render()。通过参数 :partialto 你可以用局部模板输出的内容来修改 DOM 元素,或者通过 :inline 参数使用内部模板。每次新的 thought logged 出现在所有以前存在的 logged thoughts 之后。

接下来一个黄退的视觉效果应用在 thoughts <div>。visual_effect() 方法接受第三个参数是一个hash。最后,一个 RJS 类 proxy 用来重置表单,proxying 调用 Prototype 静态表单的辅助方法 Form.reset() 并传递给它表单的id。下一章我们会详细讨论proxies。
第三章
RJS 和 Rails

如同 Rails 中的其它部分一样,RJS 很好的被合并入框架。这一章,我们看看 RJS 模板如何适应 Rails 的 rest 风格。
调式

很幸运, Rails 提供了一套机制帮助你做代码的调试工作,你可以很快的定位 bug。
开发模式调试

默认情况下,在开发模式中全部的 RJS 调用都被包装到 JavaScript try/catch 代码块中try/catch 代码块捕获在 RJS JavaScript 应答执行期间的全部异常。当异常出现时,详细的错误信息出现在两个警告框中。第一个警告框显示异常情况的详细信息。第二个警告框显示出现问题的代码。

在 environments/development.rb 文件中修改 config.action_view.debug_rjs 可以配置调试模式。如果你希望在开发模式中禁用调试,可以把这个参数设为 false 然后重启 web server。
监控日志文件

如果 Rails 在 Ajax 请求中出现了一个未经处理的异常,响应返回的是一个 HTML 错误页面而不是我们期望的 JavaScript 代码。有一个很轻松的办法来调式这个问题那就是监视日志。在 Unix 或 Linux 下有一个简单的办法来监视日志,tail 命令。在你项目的根目录执行如下代码,让 tail 来监测日志。
cody> tail -f log/development.log


-f 标记告诉 tail 把输出的信息添加到日志中。
ActionController

RJS 模板和 Rails 框架完美的结合在了一起,就像 RHTML 和 Builder 模板一样。这意味着 RJS 模板可以像其他类型的模板一样被渲染。同样的,在控制器的 actions 中 render 的大部分选项都可以使用。随后我们要看看 RJS 是如何协同 ActionController 工作的。
默认视图

默认情况下,控制器回去寻找当前正在被执行的 action 同名的模板文件。需要注意的是控制器在寻找 .rjs 模板之前会首先寻找 .rhtml 或 .rxml 模板。如果你有一个名称为 product.rhtml 的模板和一个 product.rjs 模板,ActionController 会渲染 product.rhtml 并发送将响应 Content-Type 头部设置为 text/html。这样它不会产生你期望得到的结果,并且你在日志中看不到任何错误信息。如果你现在既有 product.rhtml 又有 product.rjs 模板,你可以在 action 中指定你期望渲染哪一个模板类型。
def product
 # skip product.rhtml or product.rxml if either exists
 render :action => "product.rjs"
end

像你从代码中看到的一样,我们明确的指定了我们需要渲染 RJS 模板。另一个方法是用 respond_to() 代码块配置你期望返回的响应类型。respond_to() 代码块会给 HTTP 响应头部来设定适合的响应类型。远程 Ajax 请求由 Prototype 库指定响应头部是 text/javascript,text/html,application/xml,text/xml,*/*。它告诉 Rails,Ajax 请求喜欢 JavaScript 应答,但是同样接受其他列表中的其他应答类型。下面的代码示例帮助你理解这个概念。
def product
 respond_to do |format|
  format.html # all html requests
  format.js # all ajax requests
  format.xml # all XML requests
 end
end


上面的 respond_to() 代码块通过 Accept 头部来返回正确的响应类型,例如我们可能用到三个类型: HTML (product.html),JavaScript (product.rjs) 和 XML (product.xml)。你也可以为你能用到响应类型的来自己订制更详细的 respond_to() 代码块。
def product
 respond_to do |format|
  format.html { flash["notice"] = 'here is a product' }
  format.js { render :action => "product_rjs.rjs" }
  format.xml { render :xml => @product.to_xml }
 end
end


如上所示,你也可以自己订制更详细的 respond_to() 代码块来增加一些自己期望得到的功能。
布局

令人惊讶的,在渲染 RJS 模板时,Rails 自动就会避免渲染布局。所以当渲染一个 RJS 模板时候,你不需要在 action 中指定 :layout => false。
渲染

在 controller 内部调用 render 有很多可选的参数,使用 RJS 时你不需要担心他们。表格 1 概述了在 ActionController 中调用 render() 可选的参。
Option Works with RJS? Returns Content-Type = text/javascript?
:action Works as expected Yes
:template Works as expected Yes
:file Works as expected Yes
:inline Not useful for RJS No
:partial Not useful for RJS Yes
:text Not useful for RJS No
Inline 渲染

Rails 也支持 inline RJS 模板来渲染控制器。这样就避免了因为一个简单的一两行代码就可以完成的任务,我们却不得不创建整个 .rjs 模板文件的情况。

在控制器中的 render() 利用参数 :update 来实现渲染 Inline RJS。怎样在一个 RJS 模板的代码块中定义 render() 调用呢。代码块传递一个 JavaScriptGenerator 实例,就像普通的 RJS 模板。随后控制器的 update action 渲染局部的 header 并用其中的内容替换 id 是 header 的 DOM 元素的 innerHTML。
def update
 render :update do |page|
  page.replace_html, 'header', :partial => 'header'
 end
end


采用 inline 渲染意味着你不需要寻找正在执行的 action 动作同名的 .rjs 模板。Inline 为控制器产生视图代码。如果 inline RJS 代码超过1或者2行,那么更明智的做法是为这个功能创建一个完整的 RJS 模板。
Browser Redirection

如果在一个 Ajax 请求过程中你需要重定向浏览器,你可以看一下 Rails 的 redirect_to() 方法。Prototype 库不能像浏览器一样对 HTTP 状态代码做出反应,也不能跟随 3xx 重定向代码。还好,JavaScriptGenerator 有 redirect_to() 方法,它能为重定向浏览器产生必要的 JavaScript 代码。

这个新的 redirect_to() 方法由 page 对象在你的 RJS 模板中或控制器的 update 代码中调用。大概通常你都会在控制器的 inline RJS 中使用 redirect_to()。这个新方法和过去我们使用的标准 redirect_to() 方法没啥区别,除了这个新的方法需要由一个 page 对象调用。我们看一个简单的例子,如何在 RJS 中使用重定向。
render :update do |page|
 page.redirect_to :controller => 'employees', :action => 'list'
end


这里我们把浏览器重定向到了 EmployeesController 控制的 list 这个 action 。没什么新鲜和令人惊讶的,重定向功能和过去一样,只是它在这里是由一个 page 对象来调用,这个对象由 JavaScriptGenerator 产生,它会创建适当的 JavaScript。

你也可以重定向到一个绝对地址来。
render :update do |page|
 page.redirect_to 'http://www.shopify.com'
end 


ActionView

RJS 模板通过 update_page() 代码块渲染,代码块接受名叫 page 的 Rails JavaScriptGenerator 实例。

在我们这个 "Thought Log" 的例子中,RJS 模板在它执行之前由 ActionView 完成转换。
update_page do |page|
 page.insert_html :bottom, 'thoughts', :partial => 'thought'
 page.visual_effect :highlight, 'thoughts'
 page.form.reset 'thought-form'
end

ActionView 用两行 update_page() 代码块来包装模板的 RJS 代码。

很多 RJS 方法接受可变长度参数列表,options_for_render。如果这个参数是一个 Hash,把参数传递给 ActionView#render()。这允许你用字符串或渲染一个模板更新 DOM 元素。

在 RJS 调用中使用 :partial 选项,你也可以使用其他你在 RHTML 模板常使用的参数bject 和 :localsthat 。下面这个局部模板能渲染用户信息。
<div id="user">
 <p>Name: <%= name %></p>
 <p>Title: <%= title %></p>
</div>


这部分需要局部变量 name 和 title,所以你需要在调用 RJS 方法时正确使用参数 :locals 传递一个 hash 。
page.replace_html 'user', :partial => 'user', :locals => { :name => 'Cody Fauser', :title => 'El Presidente' }
Element, Class, and Collection Proxies


当 RJS 第一次提出来的时候,更新页面上 DOM 对象的唯一办法是调用一个页面对象并在 action 中传递期望更新的对象的 id。 Element 和 collection proxies 引入了一个新途径将 DOM 对象和使用 Proxy 设计模式结合起来。proxy 让 RJS 模板可操作一个或多个 DOM 元素。
Element Proxies

element proxy 给 JavaScriptGenerator 增加了 [] 方法。proxy 和 page 对象调用 visual_effect() 方法很像,除了这个方法会在 JavaScript 中产生一个实际 DOM 对象。proxy 对象可以理解页面中的 DOM id。下面的 RJS 模板隐藏的 id 是 header 的 DOM 元素。。
page['header'].hide

当然也可以理解符号,所以如下写法也可以正常工作。
page[:header].hide

proxy 也支持连锁方法,所以你可以定义多个连锁的方法:
page[:header].hide.show
Class Proxies

RJS class proxy 提供使用 JavaScript class 方法。这些方法可能定义在 Prototype 中,例如 class Form,或者定义在你自己的脚本中。而不是根据 DOM 的 id 来选择一个对象,和 element proxies 一样,page 对象在使用 class proxy 时可以使用连锁方法。

一个实际的应用是采用 class proxies 的方式用 Prototype's Form class 处理表单。Form class 支持表单的 disabling, enabling, resetting, 和其他很多功能:
page.form.reset('employee-form') # => Form.reset("employee-form");


然而,class proxies 不是仅仅能依靠 Prototype 工作。你可以利用你自己的静态 JavaScript 方法调用它。也就是说你可以在你的 public/javascripts/application.js 文件中定义 JavaScript class。
var Alerter = {
 displayMessage: function(text) {
  alert(text);
 }
}


使用 Alerter 类定义,你可以在 RJS 模板中使用 RJS class proxying 来调用静态的 displayMessage() 函数。
page.alerter.display_message('Welcome') # => Alerter.displayMessage("Welcome");

class proxies 为结合 JavaScript 提供了一个很好的桥梁,以便你可以压缩你的 JavaScript 库并且在 RJS 中方便的使用。
Collection Proxies

select() 方法返回一批 DOM 对象。select() 根据一个 CSS-based 选择器并且返回一组符合条件的 DOM 对象。所以如果你想得到 id 是 content 的 <div> 中全部的段,可以采用如下方法:
page.select('#content p')

更进一步,你可以:
page.select('#content p').each do |element|
 element.hide
end


把 id 是 content 的元素内的所有 <p> 元素均被隐藏。

最近增加了一个支持 attribute-based 选择器的功能,允许你根据 CSS 属性来选择所需的元素。
page.select('#form input[type=text]').each do |element|
 element.hide
end


这个代码会隐藏全部父元素的 id 属性是 form 并且 type 值是 text 的 input 元素,

属性选择器支持 =, ~=, |=, existence 和 != 你可以使用属性选择器的同时使用这些,例如 input[class=link][href="#"]。可惜 IE 不支持这些选择器。
在 Rails 中使用 Ajax

如果页面中不使用 Ajax 的话 RJS 不是很有用处。Rails 根据不同的环境提供了许多方法来实现 Ajax。可以看看 Ruby on Rails 文档中关于这些方法的详细说明。
link_to_remote(name, options = {}, html_options = {})

在 Rails 中这是生成 Ajax 的最常用的办法。当点击 link_to_remote() 生成的超链接的时候产生一个 Ajax 请求。当 <a> 的 onclick() 事件发生时, Rails 产生一个 Ajax.Request 或 Ajax.Updater,依靠传递给 link_to_remote() 的 :update 参数。在 RJS 中,:update 选项不会产生 Prototype Ajax.Updater 对象。如果页面中的 RJS 返回的结果出现问题,可以看看是否在 RJS 模板中错误的使用了 :update 选项。
link_to_function(name, function, html_options = {})

当点击超链接的时候执行 JavaScript 函数或代码。这实际上不会创建 Ajax 请求,但是它可以执行自己编写的 JavaScript 函数。使用这个方法来执行自己编写的 JavaScript 库,在其中利用 Ajax.Request 来构造 Ajax 调用。
remote_function(options)

产生 JavaScript 在后台生成一个 Ajax 请求访问控制器的 action。这个方法用在操作 DOM 对象事件的时候发起 Ajax 调用是非常有用,例如 <select> 标签的 onchange() 动作。接受和 link_to_remote() 方法一样的参数。
observe_form(form_id, options = {})

工作原理和 observe_field() 一样,但是侦测整个表单。
form_remote_tag(options = {})

创建一个表单,在背后使用 Ajax 请求来提交其中的内容。这是另一个非常有用的生成 Ajax 请求的方法。
form_remote_for(object_name, object, options = {}, &proc)

和 form_remote_tag() 一样,Rails 1.1 提出使 form_for 更符合语义。
submit_to_remote(name, value, options = {})

创建一个 button 来提交父表单中的内容到一个远程的控制器 action。接受的参数和 form_remote_tag() 一样。
in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {})


当修改输入框的字段时候,产生一个 Ajax 请求。在 RJS 中使用这个方法需要给 the in_place_editor_options 传递一个 hash 其中指定 :script => true。
in_place_editor(field_id, options = {})

这个方法限制于 in_place_editor_field()。在 RJS 中使用这个方法需要给 options 传递的 hash 参数有 :script => true。
drop_receiving_element(element_id, options = {})

当删除一个元素的时候产生一个 Ajax 请求。
sortable_element(element_id, options = {})

当元素使用拖放功能的时候随时产生 Ajax 请求。
Ajax.Request(url, options)

所有的 Rails 辅助方法都使用这个 Prototype 对象来生成真实的 Ajax 请求。你也可以在你的 JavaScript 中使用这个对象发起 Ajax 请求。这个 JavaScript 对象不是 Ruby 对象。
辅助方法

Rails 辅助方法让 RJS 模板可以在一个方法中操作多个。不同于传统的辅助方法,RJS 辅助方法必须传递给一个 page 对象。
def insert_item(list_id, item)
 page.insert_html :bottom, list_id, '<li>#{item.title}</li>'
 page.visual_effect :highlight, 'item_#{item.id}', :duration => 0.5
end


现在,不必在每个模板中总是使用 insert_html() 和 visual_effect(),我们可以使用 insert_item() 方法,它能帮助我们写出更简单易读的模板。这个新的辅助方法可以在如下的 RJS template 调用, @item 是一个传递给 RJS 模板的实例变量,我们希望把每一个 item 追加到 my_list 列表中:
page.insert_item 'my_list', @item 


第四章
RJS 实践: The Expense Tracker

迄今为止我们完成了 "Thought Log" 应用,也初步了解了 RJS 是如何融入 Rails 框架中的。现在我们再做一个例子,这个例子更贴近真实并且会解决很多你在自己项目中可能会遇到的问题。为了不让我们的开发支出脱离我们的掌控,所以现在就来创建一个简单的应用帮助我们来追踪这些。
创建 Models

首先,我们用 Rails 的模型生成器来创建我们项目要用到的模型。Rails 的模型生成器会自动创建模型和相关的迁移文件。我们只需要在其中增加我们需要的功能就可以。
expenses> ruby script/generate model Project
     exists app/models/
     exists test/unit/
     exists test/fixtures/
     create app/models/project.rb
     create test/unit/project_test.rb
     create test/fixtures/projects.yml
     create db/migrate
     create db/migrate/001_create_projects.rb

expenses> ruby script/generate model Expense
     exists app/models/
     exists test/unit/
     exists test/fixtures/
     create app/models/expense.rb
     create test/unit/expense_test.rb
     create test/fixtures/expenses.yml
     exists db/migrate
     create db/migrate/002_create_expenses.rb


发生器创建了 app/models/project.rb 和 app/models/expense.rb 两个文件,并分别在其中定义了 Project 和 Expense 模型,包括单元测试和测试夹具。发生器也帮我们创建了迁移文件 db/migrate/001_create_projects.rb 和 db/migrate/002_create_expenses.rb。

现在模型生成器为我们创建了两个新的迁移文件,我们要在其中增加需要用到的字段定义。打开 db/migrate/001_create_projects.rb 像下面代码一样编辑:
class CreateProjects < ActiveRecord::Migration
 def self.up
  create_table :projects do |t|
   t.column :name, :string
  end
 end

 def self.down
  drop_table :projects
 end
end


我们在迁移中增加一行,t.column :name,:string。这行代码在 projects 表中增加了名字是 name 字段类型是 String 的字段。下一步,为表 expenses 定义字段。同理,打开 db/migrate/002_create_expenses.rb 文件然后增加 project_id,description 和 amount 的字段定义。
class CreateExpenses < ActiveRecord::Migration
 def self.up
  create_table :expenses do |t|
   t.column :project_id, :integer
   t.column :description, :string
   t.column :amount, :float
  end
 end

 def self.down
  drop_table :expenses
 end
end


当数据库创建完毕并且连接配置成功,我们可以运行迁移文件。它将根据迁移文件中的字段定义在 config/database.yml 中定义的开发数据库内创建两个表。
expenses> rake migrate

现在 Expense Tracker 需要的数据库部分都搞定了,我们可以定义模型之间的关系。 一个 Project 对应许多 Expense 对象,所以在文件 app/models/project.rb 中的 Project 模型里增加 has_many() 关系。
class Project < ActiveRecord::Base
 has_many :expenses, :dependent => :delete_all
end


我们在 has_many() 后调用了 :dependent => :delete_all 参数,因为我们数据中不希望存在一个 expenses 而他却不属于任何一个 Project。打开 app/models/expense.rb。现在,在 Expense 模型中定义 belongs_to() 关系。一个 Expense 对象 belongs_to() 一个 Project。因为 Expense 包含外键。
class Expense < ActiveRecord::Base
 belongs_to :project
end

现在模型和数据库都定义好了,我们可以进行下一步开发控制器部分。
定义 Controllers

先生成两个控制器。第一个为了 projects,滴二个为了 expenses。
expenses> ruby script/generate controller Projects
     exists app/controllers/
     exists app/helpers/
     create app/views/projects
     exists test/functional/
     create app/controllers/projects_controller.rb
     create test/functional/projects_controller_test.rb
     create app/helpers/projects_helper.rb

expenses> ruby script/generate controller Expenses
     exists app/controllers/
     exists app/helpers/
     create app/views/expenses
     exists test/functional/
     create app/controllers/expenses_controller.rb
     create test/functional/expenses_controller_test.rb
     create app/helpers/expenses_helper.rb


再一次,使用发生器简单的就为 project 创建出了所需的文件,包括模板视图的文件夹和控制器相关的测试。

show() 动作根据 params Hash 中传递回来的 :id 参数来找到 Project 对象。编辑
app/controllers/projects_controller.rb 文件,然后增加如下代码。
class ProjectsController < ApplicationController
 def show
  @project = Project.find(params[:id])
 end
end


接下来,编辑 app/controllers/expenses_controller.rb 增加创建 Expense 对象的代码。
class ExpensesController < ApplicationController
 before_filter :find_project

 def new
  @expense = @project.expenses.create(params[:expense])
 end

 private
 def find_project
  @project = Project.find(params[:project])
 end
end


ExpensesController 比 ProjectsController稍微复杂一点。既然每一个 Expense 对象都属于一个 Project,我们可以用 before_filter 保存很多相同的代码。before_filter 在每一个控制器的动作执行之前执行。我们定义的过滤器,自动根据 params Hash 中的 :project 键找到 Project,并将其储藏在变量 @project 中。

new 动作,就跟其名字所表达的意义一样,给一个 Project 增加一个 Expense 对象。Rails 在 RJS 模板方面遵循和 RHTML 和 RXML 模板一样的约定。Rails 根据控制器的动作名称去寻找同名的模板文件。那么,当触发 new 动作时,控制器自动寻找 app/views/expenses/new.rjs 模板。
设置路由

接下来我们设置 before_filter 和创建更漂亮的 URLs。controller 根据 params[:project] 传递的值来寻找 project。我们需要增加一个简单的路由规则来产生类似 /projects/1/expenses/new 的漂亮的 url 。

打开 config/routes.rb 在默认的路由设置之下增加如下代码:
map.expenses 'projects/:project/expenses/:action/:id', :controller => 'expenses'

在开发模式下,路由的改变会立即生效。继续,我们给应用创建一个布局模板。
创建一个应用布局

我们需要一个布局来包含我们的内容。ActionController::Base 后代自动的根据控制器的类名寻找一个布局模板。这意味着 ApplicationController 会自动寻找一个名为 application.rhtml 的布局模板。我们的控制器都是 ApplicationController 的后代,所以会很好的继承下来,除非隐藏。创建 app/views/layouts/application.rhtml 并在编辑器中增加如下代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <title>Expense Tracker</title>
  <%= stylesheet_link_tag "screen.css" %>
  <%= javascript_include_tag :defaults %>
 </head>
 <body>
  <div id="content">
   <%= yield %>
  </div>
 </body>
</html>


注意,我们使用 Rails 辅助方法 stylesheet_link_tag() 来包含样式表 screen.css,即 public/stylesheets/screen.css。我使用了一些简单的风格,代码如下:
th { text-align: left; }
#content { margin: 10px; }
#content p { margin-bottom: 10px; }
#expenses,#summary { border: none; border-collapse: collapse; width: 600px;}
.amount { width: 40%; }
.amount, .total { text-align: right; }
#new-expense { margin-top: 2em; background-color: #eeede5; padding: 1em; }
#new-expense h3 { margin-top: 0.5em; }
#total { border: none; border-collapse: collapse; width: 600px;}
#total-amount, .total { font-weight: bold; background-color: #eeede5; }
#total-amount { border-top: 2px solid black; }
.total { width: 90%; padding-right: 10px; }

插入数据

我们需要一些数据来显示我们的页面,所以我要输入一些数据。运行控制台。
expenses> ruby script/console
>> rjs_book = Project.create(:name => 'RJS Templates for Rails')
=> #<Project:0xb72e444c8 ...>
>> rjs_book.expenses.create(:description => 'Americano at Bridgehead', :amount => 1.93)
=> #<Expense:0xb72cb84c ...>
>> rjs_book.expenses.create(:description => 'Sandwich at La Bottega', :amount => 4.27)
=> #<Expense:0xb72c3b24 ...>
>> quit


我不认为我侥幸逃脱了费用支出,但是至少我的钱花到了更合适的地方。现在这些样例数据需要一些视图。
创建视图

我们还没有为我们的 Project 视图而进行任何开支。我们需要创建一个视图来展示 Expense 对象,例如刚创建的 Project 。视图分为一个模板和两个局部模板。我们明确分离 _expense.rhtml 局部,所以当用 RJS 更新表格时,我们可以渲染一个单独行。

创建视图文件 app/views/projects/show.rhtml 并增加如下代码:
<h1><%= @project.name %></h1>

<h2>Expenses</h2>
<table id="expenses">
 <tr><th>Description</th><th class="amount">Amount</th></tr>
 <%= render :partial => 'expenses/expense', :collection => @project.expenses %>
</table>

<%= render :partial => 'expenses/new' %>


这个局部模板给了表格一个 id expenses,因此我们可以在更新页面时查找到它。我们给局部模板一个相对路径,因为我们从 ProjectsController 渲染 show.rhtml,但是局部模板在 app/views/expenses 视图文件夹中。

现在我们创建局部模板 app/views/expenses/_expense.rhtml。这个局部模板在 <table> 内部渲染实际的 Expense 模板。

<tr id="expense-<%= expense.id %>">
 <td><%=h expense.description %></td>
 <td class="amount"><%=h number_with_precision(expense.amount, 2) %></td>
</tr>


注意,我们是如何用 Expense 对象的 id 属性来设置每一行的 id。我们这么做的原因和我们给 <table> 一个 id: 一样,它允许我们更新每一行。number_with_precision() 是一个内置的 Rails 辅助方法用来根据参数中来显示小数位数。我们也使用 h() 方法,避免 HTML 破坏页面。转义 HTML 防止用户在 project 的 title 中提交 JavaScript 等恶意的代码。

最后,我们增加局部模板到表单中。将如下代码放入 app/views/expenses/_new.rhtml 中:
<div id="new-expense">
 <h3>Add an expense</h3>
 <% form_remote_for :expense, Expense.new, :url => hash_for_expenses_url(:project => @project, :action => 'new'),
          :html => { :id => 'expense-form' } do |f| %>
  <label for="expense_description">Description:</label><br />
  <%= f.text_field 'description', :size => 60 %><br />

  <label for="expense_amount">Amount:</label><br />
  <%= f.text_field 'amount', :size => 10 %><br /><br />

  <%= submit_tag 'Add Expense' %>
 <% end %>
</div>

这不是一个规则的表单。我们使用 form_remote_for() 方法,它用 Ajax 请求在后台提交数据到控制器的动作。第一个参数传递对象的名字;表单数据储存在 params Hash 中这个键值下。第二个参数是对象,提供给表单的初始数据。然后我们传递 :url 选项,它告诉表单将数据提交到哪里。注意,我们调用路由辅助方法 hash_for_expenses_url(),它产生根据 expenses 在 config/routes.rb。:url Hash 的参数用来调用这个方法。我们也给 form 一个 id 名为 expense-form 因此 RJS 和 JavaScript 代码可以引用 from 。

最后,我们创建一个 RJS 模板当我们增加一个新的 expense 时更新我们的 project 的 expense 页面。创建 app/views/expenses/new.rjs 并增加如下代码:
page.insert_html :bottom, 'expenses', :partial => 'expense'
page.visual_effect :highlight, "expense-#{@expense.id}"
page.form.reset 'expense-form'


第一行代码用局部模板 _expense.html 生成的 HTML 插入 id expenses DOM 元素的下面。选项 :bottom 指定局部模板生成的 HTML 将插入元素现有内容的下面。因为有一个实例变量 @expense 并且局部模板名为 expense,@expense 自动的在局部模板内部使用局部变量 expense 来替换。因此我们通过选项bject => @expense 传递。

第二行代码请求一个 Scriptaculous 视觉效果加亮一个新的 Expense 对象。最后,第三行代码用 RJS 类来复位表单。接下来,我们做一个测试驱动开开发生了什么。

第六章
增强 Expense Tracker

Expense Tracker 现在使用 Ajax 在后台调用来给 Project 增加一个 Expense 对象。虽然表单工作正常并且成功的为 project 增加了 expenses ,它还可以做的更好。接下来我们将要给页面增加一个行为指示器,并且稍后,我们增加一个概要章节来展一些关于 project 的统计表。
Ajax 行为指示器

Ajax 的一个问题是它违反了用户习惯的浏览习惯。当用户执行一个与服务器交互的动作后,更习惯看到重新加载整个页面。没有任何的指示表明页面是忙碌的会让用户觉得很奇怪。用户或许觉得页面没有反应并重新点按钮,结果就是想象之外的效果了。

解决这个问题的方法之一是在页面上给出一些提示,让用户知道正在发生一个远程调用。这个例子中,我们使用动态的 GIF ,并附带一些描述行的文字。我们也可以禁用表单以避免当表单提交数据的时候,用户再次点击提交按钮而再次提交数据。

我们可以当 Ajax 回调的时候,利用 JavaScript 完成这些功能。 Rails 允许你在远程调用的过程中加入一些选项来参与回调。可能用到的回调包括: :uninitialized, :loading, :loaded, :interactive, :complete, :failure 和 :success。可以看看 Rails 文档仔细了解关于这些的详细信息。

创建 public/javascripts/application.js 然后增加如下代码:
var ExpenseTracker = {}

ExpenseTracker = {
 disableExpenseForm: function() {
  Element.show('form-indicator');
  Form.disable('expense-form');
 },

 enableExpenseForm: function(form) {
  Element.hide('form-indicator');
  Form.enable('expense-form');
 }
}


我们创建了一个新的 JavaScript 对象 ExpenseTracker,我们的页面会用到它。然后我们增加一个简单的方法,disableExpenseForm(),用来显示一个自转的指示器并在后台发生 Ajax 请求时禁用表单。我们也增加了 enableExpenseForm(),用来隐藏指示器并在调用完成后启用表单。我们可以立即调用这些方法而不需要 ExpenseTracker 对象,但是我们将来需要为每个方法增加更多的功能并把这些功能压缩到一起。事实上我也喜欢用代码来管理表单这样不会让 RJS 模板变散乱。

因为我们使用 javascript_include_tag :defaults 在我们的布局模板中,Rails 巧妙的包含了 public/javascripts/application.js 连同 Rails JavaScript 库。现在我们那个简单的 JavaScript 函数已经准备好了,我们可以在回调发生的时候引发这个钩子函数。展开 app/views/expenses/_new.rhtml 并修改它使其包含回调。 form_remote_for() 方法调用的完整代码如下:
<% form_remote_for :expense,
         Expense.new,
         :url => hash_for_expenses_url(:project => @project, :action => 'new'),
         :loading => 'ExpenseTracker.disableExpenseForm()',
         :complete => 'ExpenseTracker.enableExpenseForm()',
         :html => { :id => 'expense-form' } do |f| %>


表单有 id expense-form 所以我们可以用 JavaScript 函数来更新它。我们在 RJS 模板中用 enableExpenseForm() 方法也能达到同样的效果,但是它能更好的维持回调代码在 ExpenseTracker 对象中。这也是在 RJS 模板中管理表单的技巧。我们也能在 enableExpenseForm() 方法中重置表单,但这会让表单一直重置。我们可能希望控制表单何时重置,因此用户在验证失败或发生其他问题是不会再提交表单数据。

接下来,在 submit_tag() 访问 app/views/expenses/_new.rhtml 后增加指示器图片。设置初始样式 display:none ,这样当页面第一次载入的时候指示器不会被显示出来。我的指示器只是一个简单的图片模拟 Mozilla Firefox's 自转指示器。我将图片放置在项目中的 public/images 文件夹。
<%= image_tag 'indicator.gif', :id => 'form-indicator', :style => 'display:none;' %>

现在当提交表单的时候,浏览器会显示一个自转的图片并禁用表单。这样形象的告诉了用户,正有一个 Ajax 调用在进行中用户也无法在这时重复提交表单。当调用完成后,指示器隐藏起来并且表单不再禁用。在这个例子中,整个过程的发生其实很快以至于你可能还没有意识到指示器的显示和表单的禁用这个过程。你可以在 ExpenseController 的 new() action 中增加一个 sleep() 来延缓这个过程。显然你只是在开发模式下用来测试这个过程。sleep() 代码如下:
class ExpensesController < ApplicationController
 before_filter :find_project

 def new
  @expense = @project.expenses.create(params[:expense])
  # Sleep for 3 seconds
  sleep 3
 end

 private
 def find_project
  @project = Project.find(params[:project])
 end
end

Ajax 全局应答

form_remote_tag() 的 :loading 和 :complete 回调可以很好的显示和隐藏表单指示器。这里有个问题就是如果在页面中有很多的 Ajax 功能,为每一个远程操作增加 :loading 和 :complete 显示和隐藏图片指示器的过程是很单调的。这样 Ajax 全局应答就用得上了。

Prototype 公共 Ajax 响应是一个给每个 Ajax 响应放置 JavaScript 函数的好地方。Ajax 公共调用是由 Prototype 库提供的;他们允许你为所有的 Ajax 调用采用多种多样的钩子函数。让我们移除用 :loading 和 :complete 回调来显示和隐藏指示器的代码,而用 Ajax 公共响应来代替。

代替每一个表单旁的图片指示器,Ajax 公共响应允许你在页面上设置一个单一的指示器,他会在每一个 Ajax 响应的期间显示。

设置一个 Ajax 公共响应是很简单的。你可以增加如下代码到 public/javascripts/application.js 中:
Ajax.Responders.register({
 onCreate: function() {
  if (Ajax.activeRequestCount > 0)
  Element.show('form-indicator');
 },
 onComplete: function() {
  if (Ajax.activeRequestCount == 0)
   Element.hide('form-indicator');
 }
});


代码很简单。Ajax.Responders.register() 获取一个匿名的 JavaScript 对象,它的属性名是一个 Ajax 回调,值是一个 JavaScript 函数。我们定义的一个函数会在每一个 onCreate() 回调中执行,另一个函数会在每个 onComplete() 回调过程中执行。第一个函数当有一个或多个活动的 Ajax 请求是显示 id 是 form-indicator 的 DOM 元素。第二个函数当没有 Ajax 活动的时候隐藏指示器。

现在我们可以移除 public/javascripts/application.js 中显示和隐藏指示器图片的 ExpenseTracker 对象,或者我们可以完全移除 ExpenseTracker JavaScript 代码并只在回调内部写入代码。如果我们消除了 ExpenseTracker JavaScript 对象并简单的在 app/views/expenses/_new.rhtml 代码内部嵌入 form_remote_for:
<% form_remote_for :expense,
         Expense.new,
         :url => hash_for_expenses_url(:project => @project, :action => 'new'),
         :loading => 'Form.disable("expense-form")',
         :complete => 'Form.enable("expense-form")',
         :html => { :id => 'expense-form' } do |f| %>


现在回调可以启用和禁用表单。该代码用来显示和隐藏的指示器,由 Ajax 公共响应来实行。每当有 Ajax 活动的时候指示器显示,而当 Ajax 调用完成后,指示器隐藏起来。我们可以增加更多的功能,例如把指示器图片移到屏幕的一角,或使用一个活动的高亮图片。现在,我们只把图片放到同一个地方。

Ajax 公共方法提供了一个非常好的在每一个 AJAX 的请求的生命周期执行动作的方法。这不仅减少了代码的重复,这使得我们的模板,更容易理解的,而且,也节省了大量的打字。
Model 验证

在当前的状态下,Expense Tracker 会接受任何形式的输入并试图创建 Expense 对象。问题是应用阻塞输入验证。最有可能的情况是 ActiveRecord 抛出一个异常,它不是我们的代码。我们增加的 Ajax 指示器还在那里自转而用户不知道发生了什么。很幸运,Rails 有不错的 model 验证。我么可以验证新的 Expense 对象并用一个漂亮的提示框包含给用户的信息。让我们为 app/models/expense.rb 中的 Expense model 增加一些验证吧:
class Expense < ActiveRecord::Base
 belongs_to :project

 validates_presence_of :description
 validates_numericality_of :amount

 protected
 def validate
  errors.add(:amount, "must be greater than 0") unless amount.nil? || amount >= 0.01
 end
end


这个验证代码保证 description 字段不为空,Expense 十一个数字对象并且大于0。现在我们稍微修改一下我们的 RJS 模板来显示错误信息。打开 app/views/expenses/new.rjs 然后按下面的代码修改:
if @expense.new_record?
 page.alert "The Expense could not be added for the following reasons:\n" +
      @expense.errors.full_messages.join("\n")
else
 page.insert_html :bottom, 'expenses', :partial => 'expense'
 page.visual_effect :highlight, "expense-#{@expense.id}"
 page.form.reset 'expense-form'
end


这个代码验证 Expense 对象是否仍是新记录。如果它是一个新对象,那肯定是在储存的时候有一些问题并显示错误信息。否则,一个普通的插入动作会执行插入一个新 Expense。注意仅当操作成功时表单重置。这样,当出错的时候用户不用再次重复输入 description 和 amount。

在本例中,我们只用了一个简单的JavaScript警告框,以显示错误。这是一个在 RJS 中显示错误的简单方法。另一种解决办法,将取代整个表格,并用 error_messages_for() 在页面上输出错误信息。这充分利用的 Rails 内置的辅助方法,但是也可需要在 RJS 模板中做更多的修改,因为你将不得不在 Expense 对象成功添加后删除或隐藏错误信息。
增加一些考虑

如果当请求发生的过程中 Expense 表单禁用,无疑会提高可用性。但是有很长的路要走。我还不知道 project 的 Expenses 总数是多少。同样,我们添加一些代码来显示其它需要注意的数据,比如:Expense 的最小值,Expense 的最大值,以及 project 中 expense 的平均值。我们将必须确保在给 project 增加 expenses 时,所有被添加的信息都得到更新。

首先,要增加一些考虑方法在 Project 模型中。打开 app/models/project.rb 增加 calculation 方法。你的模型开起来像下面这样:
class Project < ActiveRecord::Base
 has_many :expenses, :dependent => :delete_all

 def total_expenses
  expenses.sum(:amount)
 end

 def min_expense
  expenses.minimum(:amount)
 end

 def max_expense
  expenses.maximum(:amount)
 end

 def avg_expense
  expenses.average(:amount)
 end
end

These methods are all ridiculously simple. We use the power of the new Active Record Calculations (added in Rails 1.1) to do all of the dirty work. Notice that the calculation methods are being called from the expenses collection. Calling each calculation from the collection instead of from the Expense class causes the calculation to be scoped to the current Project, which is what we want in this case. We pass in the Symbol :amount to each calculation because that is the Expense attribute on which we want to perform the calculation.

We might as well display all of this information on the page that shows the project's expenses. We can show the total expenses using a partial that we'll render directly under the list of expenses. Create app/views/expenses/_total.rhtml, which will look like this:
<table id="total">
 <tr>
  <td></td>
  <td class="total">Total</td>
  <td id="total-amount" class="amount"><%= number_to_currency(total) %></td>
 </tr>
</table> 
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics