homeASCIIcasts

196: 嵌套模型的form第一部分 

(view original Railscast)

Other translations: En Es It Fr

Other formats:

Written by allenwei (allenwei.cn)

在2007年的一系列 episode中涵盖了创建能管理多个model的复杂的form。这一系列现在已经过时了,所以从这期episode我要给你们展示最新的处理这种多个model的form的方法。

使我们的处理这个问题的方法的产生差异的原始是accepts_nested_attributes_for方法,这个方法是在Rails 2.3增加的。我们在整个系列都要用这个方法,所以你必须运行最新版本的Rails,才能把这项技术应用于你的项目中。

accepts_nested_attributes_for文档是值得一读的,文档中介绍了如何在一个update请求中更新嵌套属性,但是文档中对于如何让它在view中工作讲得不清除,所以我们主要关注这方面。

我们的调查程序

让我们先看一下我们我们要在这集建立的调查程序。该应用程序将有一个用于创建和编辑Survey(调查)的复杂的form,这个form让我们输入调查的名字,若干问题,每一个问题有多个答案。这个form有一些links,允许我们从一个调查中动态的创建和删除问题和答案。

用于创建和编辑调查的复杂形式。

用于创建和编辑调查的复杂form

我们在这里有一个深层嵌套的关联,一项调查中,有许多问题,问题有许多答案。这个在之前的复杂form的系列中是不可能的,不可能创建象这样的深层嵌套的form,但Rails 2.3 以后我们可以。

入门

我们将从头开始创建我们的调查申请,因此我们将首先创建一个新的Rails应用程序surveysays

rails surveysays

为了使编写应用程序更容易使用,我们将使用两个 Ryan Bates 的 nifty generators。我们将使用nifty layout generator,创建一个应用程序的layout。

script/generate nifty_layout

我们的应用程序将有三个model: SurveyQuestionAnswer 。我们将开始与Surveymodel,并使用nifty scaffold generator去创建sacaffold。Survey将只有一个属性叫name

script/generate nifty_scaffold survey name:string

接下来,我们运行migrate去创建surveys表。

rake db:migrate

因为我们有scaffold当我们查看这个应用程序的时候,我们能列出,创建和编辑survey,并且我们有基本survery form。

断头台生成的基本调查表。

我们想在form上出现那些我们可以给survey添加问题和答案的field。第一步,我们将生成Question model。这将有一个survey_id field 和用来放置问题内容的 content field。

script/generate model question survey_id:integer content:text

完成后,我们将再次migrate数据库,创建questions表。

rake db:migrate

接下来,我们将在他们各自的model文件上建立SurveyQuestion 之间的关联。

/app/models/question.rb
class Question < ActiveRecord::Base
  belongs_to :survey
end
/app/models/survey.rb
class Survey < ActiveRecord::Base
  has_many :questions, :dependent => :destroy
end

请注意,在Survey ,我们使用:dependent => :destroy ,当我们删除了一项调查其所有问题都被删除了。

Survey的model,我们将使用accepts_nested_attributes_for,它使我们能够通过Survey 管理 questions。利用这一点,当我们更新调查的属性的时候,我们可以创建,更新和删除问题。

/app/models/survey.rb
  class Survey < ActiveRecord::Base
    accepts_nested_attributes_for :questions
    accepts_nested_attributes_for :questions
end

创建form

建立SurveyQuestion的model后,我们将建立调查的 form我们想要做的,给每一个调查的问题在form上创建一些fields。我们可以使用fields_for方法在一个form中管理关联的fields,传递给它的关联的model的名称,循环所有关联的记录,然后为他们创建form builder。这个builder会为每个问题渲染一个label和一个textarea。

/app/views/survey/_form.html.erb
<% form_for @survey do |f| %>
  <%= f.error_messages%>
  <p>
    <%= f.label :name %><br />
    <%= f.label :name %><br />
  </p>
  <% f.fields_for :questions do |builder| %>
  <p>
    <%= builder.label :content, "Question" %><br />
    <%= builder.text_area :content, :rows => 3 %>
  </p>
  <% end %>
  <p><%= f.submit "Submit" %></p>
<% end %>

当我们刷新form的时候,它看起来像以前一样。这是因为一项新的调查将不会有任何问题与之关联,因此,没有问题的field被显示出来。最终我们希望在form上有一个”Add Question“的链接,但现在我们通过SurveyController的new acton 建立一些问题。

/app/controllers/surveys_controller.rb
def new
  @survey = Survey.new
  3.times { @survey.questions.build }
end

上面的代码将会给一个调查建立三个问题,当我们刷新页面时我们就会看到。我们现在可以填写姓名和前两个问题,然后提交新的调查。

填写新的调查表。

当我们的提交后,一个新的Survey记录将被创建,但因为我们没有在页面上展示问题,所有我们不会看到它的问题。为了解决这个问题,我们可以修改的Survey show,显示一项调查的所有问题。

这些问题不是显示在调查网页。
/app/views/survey/show.html.erb
<% title "Survey" %>

<p>
  <strong>Name:</strong>
  <%=h @survey.name %>
</p>

<ol>
  <% for question in @survey.questions %>
  <li><%= h question.content %></li>
  <% end %>
</ol>

<p>
  <%= link_to "Edit", edit_survey_path(@survey) %> |
  <%= link_to "Destroy", @survey, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", surveys_path %>
</p>

当我们重新刷新调查的页面,我们会看到列出的问题,这说明,当我们添加调查的时候问题也被保存了。

现在的问题是显示。

我们还可以编辑的一项调查,当我们提交这个form的时候,对于任何问题的改变都会被提交。

在我们的网页上面列出的有三个问题,尽管我们只输入了前两个值。最后的空白问题如果会被自动删除,那就更好更好了。该accepts_nested_attributes_for方法有一个reject_if选项,我们可以用这个做。该方法接受一个lambda,hash属性传递给它,我们可以使用这个hash,当问题的content是空白的时候,拒绝保存问题。

/app/models/survey.rb
class Survey < ActiveRecord::Base
  has_many :questions, :dependent => :destroy
  accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? }
)end

如果我们现在创建的survey只有在只有两个问题field被添入了,那么只有这两个question会杯保存,我们不会看到列表中的空白的问题。

空白的问题是不再显示。

如果我们想删除正在编辑的调查中已经存在问题,该怎么办呢?在最终的应用程序要使用link来删除问题,但现在我们将采取更容易实现,用一个checkbox。在调查的的form partial中,我们将添加一个checkbox和一个lanel。

/app/views/survey/_form.html.erb
<% form_for @survey do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <% f.fields_for :questions do |builder| %>
  <p>
    <%= builder.label :content, "Question" %><br />
    <%= builder.text_area :content, :rows => 3 %>
    <%= builder.check_box :_destroy %>
    <%= builder.label :_destroy, "Remove Question" %>
  </p>
  <% end %>
  <p><%= f.submit "Submit" %></p>
<% end %>

这里的窍门是给name复选框属性指定_destroy。当这个复选框的value是true,复选框被选中,该记录将在表单提交之后被删除。

为了使它项工作,我们需要Survey model中开启它,在 accepts_nested_attributes_for后加入:allow_destroy => true

/apps/models/survey.rb
class Survey < ActiveRecord::Base
  has_many :questions, :dependent => :destroy
  accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
end

当我们刷新页面,我们对于每一个问题将有一个“Remove Question”的checkbox。

我们现在有复选框,这样我们可以消除的问题。

如果我们选中一个问题的“Remove Question” checkbox,提交表单,这个问题就会被删除。

这个问题已成功删除。

添加答案

现在,我们有问题了,但没有答案。我们现在通过创建Answer model,建立嵌套表单。首先,我们将生成的model。

script/generate model answer question_id:integer content:string

然后migrate 数据库。

rake db:migrate

接下来,我们将建立AnswerQuestion的关联。Answerbelong_to Question

/app/models/answer.rb
class Answer < ActiveRecord::Base
  belongs_to :question
end

对于Question ,我们将需要使用accepts_nested_attributes_for,就像我们在Survey model里做的那样。

/app/models/question.rb
class Question < ActiveRecord::Base
  belongs_to :survey
  has_many :answers, :dependent => :destroy
  accepts_nested_attributes_for :answers, :reject_if => lambda { |a| a[:content].blank? 
}, :allow_destroy => true

end

在form中,我们需要给答案添加field,但如果我们再给答案增加嵌套模型,答案的form的view将会变得混乱。将来,我们将要使用JavaScript通过link添加问题,因为这两个问题,我们要吧form部分的代码移入question_fields partial。

经过整理后的form view变成这样。

/app/views/surveys/_form.html.erb
<% form_for @survey do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <% f.fields_for :questions do |builder| %>
    <%= render 'question_fields', :f => builder %>
  <% end %>
  <p><%= f.submit "Submit" %></p>
<% end %>

请注意,我们只是传递了partial的name的字符串到render,新form的简便写法是Rails 2.3 引入的。我们以f为名字把builder传到partial。在新的question_fields partial,我们可以使用f变量去render Question的表单元素。

/app/views/surveys/_question_fields.html.erb
<p>
  <%= f.label :content, "Question" %><br />
  <%= f.text_area :content, :rows => 3 %><br />
  <%= f.check_box :_destroy %>
  <%= f.label :_destroy, "Remove Question" %>
</p>

我们可以用类似的方法处理答案部分的field,把他们放在自己的partial文件中。在_question_fields partial中,我们将要循环所有问题的答案,render新的名为_answer_fields的parital。

/app/views/surveys/_question_fields.html.erb
<p>
  <%= f.label :content, "Question" %><br />
  <%= f.label :content, "Question" %><br />
  <%= f.check_box :_destroy %>
  <%= f.label :_destroy, "Remove Question" %>
</p>
<% f.fields_for :answers do |builder| %>
  <%= render 'answer_fields', :f => builder %>
<% end %>

在我们的新_answer_fieldspartial,我们将放入一些代码去render一个Answer

/app/views/survey/_answer_fields.html.erb
<p>
  <%= f.label :content, "Answer" %>
  <%= f.text_field :content %>
  <%= f.check_box :_destroy %>
  <%= f.label :_destroy, "Remove" %>
</p>

因此,我们可以在表单上看到答案的fields,我们将修改SurveyControllernew action,三个问题中的每一个问题将会有四个答案。

/app/controllers/survey_controller.rb
def new
  @survey = Survey.new
  3.times do
    question = @survey.questions.build
    4.times { question.answers.build }
  end
end

现在,在你创建一各调查的时候,将会创建3个问题,每个问题有四个答案。

答领域正显示在调查表。

当我们填好表单,提交受答案没有出现,但我们可以很容易地解决这个问题。在调查的showview中,当我们render每一个问题的时候,我们将加入一些代码render这些问题的答案。

/app/views/survey/show.html.erb
<ol>
  <% for question in @survey.questions %>
  <li><%= h question.content %></li>
  <ul>
    <% for answer in question.answers %>
      <li><%= h answer.content %></li>
    <% end %>
  </ul>
  <% end %>
</ol>

如果我们刷新调查页面,我们将会看到问题和答案。

答案现在显示下了他们的提问。

我们还没有完全完成,因为我们希望能够通过link在form上动态添加或删除问题或答案。我们将在下一个episode涵盖。