#197 Nested Model Form Part 2
- Download:
- source codeProject Files in Zip (121 KB)
- mp4Full Size H.264 Video (17.1 MB)
- m4vSmaller H.264 Video (12.4 MB)
- webmFull Size VP8 Video (34.2 MB)
- ogvFull Size Theora Video (23.6 MB)
在上一个episode,我们展示了如何创建一个form,可以处理多个嵌套模型。我们有一个应用程序,有Survey
模型,每个Survey
有很多Questions
,每个Question
有很多Answers
。
在Survey
和Question
model,用accepts_nested_attributes_for
就使我们能够通过一个model创建,编辑和删除一条记录。
现在我们的程序只能用checkbox删除一个问题或答案。而且我们不能通过现在的form添加新的问题和答案。在这一个episode,我们将用JavaScript解决这些问题,这样我们就可以使用link动态地创建和删除这些models。
我们要写的JavaScript涉及DOM的操作,因此我们将使用Prototype库使它变得容易些。若要加入Prototype到我们的应用,我们将下面的一行到添加到我们应用程序的layout的<head>部分。
``` /app/views/layouts/application.html.erb <%= javascript_include_tag :defaults, :cache => true %> ```如果你更喜欢使用jQuery,你需要加入jQuery的引入代码,我们将在这个episode的结尾展示等价的jQuery代码。
添加link去删除回答
我们先解决最容易的部分:替换我们用来删除的问题和答案的checkbox为链接。我们先看一下问题的部分。
render问题的那部分代码,在answer_fields
partial中,看起来象这样:
<%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %>
```当我们需要删除问题的时候,我们选中上面_destroy
的复选框。我们将要用一个hidden field替换它,当我们点击“remove” link的时候,这个hidden field将被赋值。这样,我们就可以标记这个答案是需要被删除的。
我们将要把上面name为_destroy的label替换成一个link,利用Rails里的link_to_function
,这个link可以在我们点击的时候触发一个JavaScript的函数。hidden field和link部分的代码将是这个样子的:
<%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.hidden_field :_destroy %> <%= link_to_function "remove", "remove_fields(this)" %>
```当答案旁边的“删除”的链接被点击的时候,就会触发一个叫做remove_fields
的JavaScript函数,它将link作为一个参数传入,以便我们可以作为参考元素它找到答案有关的其他内容。因为没有直接得到这些field的方法,所以我们增加了一个class给这个封闭段落元素 p,使我们可以更容易地找到它们。
下一步我们需要写remove_fields
function。我们将在application.js
文件中写,因为它是会自动在我们网页中包含的文件之一,因为我们已经包括了:defaults
Javascript。
这个函数执行两个动作。首先,使用Prototype的previous
功能查找,和调这个function的link有关的,第一次出现的上一个hidden field,将其值设置为1
,答案将被标记为删除。然后,它使用up
找到这个link的DOM tree的上次第一次出现的class为“fields”的包裹这问题field的p标签,将它隐藏。所以答案也被隐藏起来了。
如果我们刷新页面,现在我们将看到每个答案对应一个link。
如果我们点击了link,_destroy
hidden field的值将被设置为1
,所以他们被表记为需要被删除的,而且它们的form fields也被隐藏了。
需要注意的是当我们点击link的时候,我们没有使用AJAX的发送我们更新后的form的value,虽然我们立即隐藏了删除的的答案,直到我们submit这个form,数据库才会得到我们的更新。当我们提交这个表单,我们将在调查show
页面页面上看到这个问题被删除了。
删除问题
现在,我们可以通过link删除答案,我们将要讨论的如何删除问题。删除答案和删除问题的方法基本相同,所以我们可以重用一些我们写过的代码。
正如我们对答案做的一样,我们要把一个checkbox和一个label替换成一个隐藏字段和一个link,我们可以从answer_fields
partial里找到那段代码,并放在一个新的叫做link_to_remove_fields
的helper方法里,传入我们希望在link中出现的lin和form变量f
。
<%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= link_to_remove_fields "remove", f %>
```现在我们在application_helper
文件里写那个方法。
创建表单字段的这个方法返回字符串,所以我们把f.hidden_field
生成的的HTML和link_to_function
方法返回的javascript连接起来,返回给partial。
我们在question_fields
partial里使用我们的新方法了。
<%= f.label :content, "Question" %>
<%= link_to_remove_fields "remove", f%>
<%= f.text_area :content, :rows => 3 %>
由于我们JavaScript function remove_fields
查找与一个class名为fields
元素,并且我们把我们的问题和答案用一个class名为fields的div
包裹起来,所以当我们点击“删除”的问题的link的时候,问题和答案都会被隐藏起来。
如果我们查看调查的edit
页面,并点击问题的“Remove” link的问题将被删除连同其答案,当我们提交表单,问题和答案将在调查中删除。
添加问题和答案
现在的困难的部分:增加了新的问题和答案。我们希望当我们点击form上的links的时候,会动态的创建一个问题和答案。让这个困难的原因是,JavaScript需要获得一个一系列空白fields的,因此,它可以在点link的时候创建一个新的问题或答复。
要做到这一点,我们将编写一个叫做link_to_add_fields
的application helper。我们在需要一个link去在form上去创建新问题和新答案的时候,可以用这个helper方法。对于该方法的代码如下所示:
该方法有三个参数: name
,这将是link的文字,f
,表单生成器对象和association
,它的值可以是“question”或“answers”。
该方法的第一行创建了一个新的关联类的新实例,即一个新的Question
或Answer
。这意味着,我们有一个模板对象,我们可以使用它来创建新的form fields。
代码的第二部分为那个对象构建一个form fields的string,以便我们能够在其中加入JavaScript函数,这个函数能在我们点击一个link的时候把他们加入到form中。它是通过调用正确的partial代码,传入form builder来实现的。唯一真正的新东西是我们设置的:child_index
。我们这样做是为了我们能引用我们新创建的问题或答案的fields。在JavaScript中,我们要给这个fields的name根据当前时间替换一个唯一的值。这样,每次我们创建了一个新的问题或答复,它将有一个唯一的索引,当我们提交表单的时候它就可以被识别了。
最后我们再次使用link_to_function
方法,传入link的name,并通过对我们传入的link调用一个JavaScript函数add_fields
,传入三个参数,link本身,association的名字,通过转义的form fields 的string。
现在我们可以回到JavaScript和编写add_fields
function。
此函数接受我们前面提到的三个参数,:被点击的link,association的名称和一个包含form fields的HTML的字符串。在这个函数的第一件事是为这个form元素创建一个新的id。如果我们创建多个新的问题或答案,我们不想让他们具有相同的index,那样当我们插入的时候,他们将被视为同一个model。我们使用当前时间作出unique的ID,然后使用正则表达式,替换form fields中的“new_question”或“new_answer”字符串为新的唯一ID。这样,我们就把form fields的字符串插入到DOM中的适当位置了。
困难的部分结束了。现在所有我们需要做的就是添加links。在question_fields
partial,我们将用link_to_add_fields
添加一个用于添加答案的link,传入:answers作为一个问题有多各答案的association的名字,
<%= f.label :content, "Question" %>
<%= link_to_remove_fields "remove", f %>
<%= link_to_remove_fields "remove", f %>
<%= link_to_add_fields "Add Answer", f, :answers %>
我们可以用类似的方法,在调查的form中添加“Add Question”的link。
``` /app/views/surveys/_form.html.erb <% form_for @survey do |f| %> <%= f.error_messages %> <%= f.label :name %>
<%= f.text_field :name %>
<%= link_to_add_fields "Add Question", f, :questions %>
<%= f.submit "Submit" %>
<% end %> ```如果我们刷新调查页面,现在我们将看到添加新的问题或答案的links,当我们点击其中一个link,新的field会出现在form上。
当我们点击“add answer”link,一个新的空白的答案的field会出现在form上。
如果我们在上面新答案的field中填入“jQuery”,并提交表单,新的答案就会被添加。
到这里,我们已经达到我们的目标,现在有一个表格,在这个form上我们可以动态地添加或删除fields,并且在我们提交表单的时候,我们正确的更新了数据库。
备用jQuery代码
我们在这集利用的JavaScript代码,是用Prototype库写的。如果您愿意使用jQuery,代码应如下所示:
``` javascript function remove_fields(link) { $(link).prev("input[type=hidden]").val("1"); $(link).prev("input[type=hidden]").val("1"); } function add_fields(link, association, content) { var new_id = new Date().getTime(); var regexp = new RegExp("new_" + association, "g"); $(link).parent().before(content.replace(regexp, new_id)); } ```此代码和我们写的Prototype的代码非常类似。
你们当中有些人读这集德时候可能是有点忐忑不安,因为我们使用的JavaScript不是unobtrusive的。而一个unobtrusive的解决办法总比没有解决方法好,这个解决方法足够简单,然我们能在这集中展示。Bates正做一个插件叫做nested_form,这将使用unobtrusive的方式使用jQuery处理nested forms。这个插件仍然在初期的发展中,如果你认为你需要像这样插件,请继续检查后续的提交。