homeASCIIcasts

196: Nested Model Form Part 1 

(view original Railscast)

Other translations: Es It Fr Cn

Back in 2007 a series of episodes covered the creation of complex forms that could manage multiple models in a single form. That series is now rather out of date so beginning with this episode we’ll show you the more modern techniques for handling forms with multiple models.

One thing that makes a big difference in our approach to handling this problem is the accepts_nested_attributes_for method which was added in Rails 2.3. We’ll be using it throughout this series so you’ll need to be running the latest version of Rails to make use of this technique in your own applications.

The documentation for accepts_nested_attributes_for is worth reading and shows how to use it with nested attributes in a single update call but it less clear on how we would get this working in the view itself, so we’ll focus on this.

Our Survey Application

Let’s first take a look at the application we want to build over the course of this series, which is an application for making surveys. The application will have a complex form for creating and editing surveys that lets us enter a survey’s name along with a number of questions, each with multiple answers. The form will also have links to allow us to dynamically add and remove questions and answers from a survey.

The complex form for creating and editing surveys.

The complex form for creating and editing surveys.

What we have here is a deeply-nested association in which a survey has many questions and a question many answers. In the previous series on complex forms it was not possible to have create deeply nested forms like this but with Rails 2.3 we can.

Getting Started

We’re going to create our survey application from scratch, so we’ll start by creating a new Rails application called surveysays.

rails surveysays

To make writing the application easier we’ll use two of Ryan Bates’ nifty generators. We’ll use the nifty layout generator first to create a layout for the application.

script/generate nifty_layout

Our application will have three models: Survey, Question and Answer. We’ll start with the Survey model and use the nifty scaffold generator to create a scaffold to go with it. Survey will have just one attribute called name.

script/generate nifty_scaffold survey name:string

Next we’ll run the migrations to create the surveys table in the database.

rake db:migrate

If we look at the application now we’ll have scaffold files to allow us to list, create and edit surveys and a basic survey form that we can build on.

The scaffold-generated basic survey form.

What we want on the form are fields that will allow us to add questions and answers when we create a new survey. The first step we’ll take is to generate the Question model. This will have a survey_id field and a content field to hold the question’s text.

script/generate model question survey_id:integer content:text

Having done that we’ll migrate the database again to create the questions table.

rake db:migrate

Next we’ll set up the relationship between Survey and Question in their model files.

/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

Note that in Survey we’ve used :dependent => :destroy so that when we delete a survey all of its questions are deleted too.

In the Survey model we’re going to use accepts_nested_attributes_for so that we can manage questions through Survey. By using this we can create, update and destroy questions when we update a survey’s attributes.

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

Creating The Form

Having set up the Survey and Question models we’ll move on to the survey form. What we want to do here is add fields for each of the survey’s questions to the form. We can use the fields_for method to manage associated fields in a form, passing it the name of the associated model and then loop through all of the associated question records and create a form builder for each of them. The builder will render a label and textarea for each question.

/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 %>
  </p>
  <% end %>
  <p><%= f.submit "Submit" %></p>
<% end %>

When we reload the form now it will look like it did before. This is because a new survey won’t have any questions associated with it and therefore none of the question fields will be shown. Ultimately we want to have an “Add Question” link on the form but for now we’ll just create some questions in the SurveyController’s new action.

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

This will add three questions to a new survey that we’ll see in the form when we reload the page. We can now fill in the name and the first two questions and submit a new survey.

Filling in the new survey form.

When we submit the survey a new Survey record will be created but we won’t see its questions as we’re not displaying them on the page. To fix this we can modify the show view for Survey to show a survey’s questions.

The questions aren't shown on the survey page.
/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>

When we reload the survey’s page we’ll see the questions listed which shows that when we added the survey its questions were saved too.

The questions are now shown.

We can also edit a survey and if we change any of the questions they will be updated when we submit the form.

On the page above we have three questions listed even though we only entered values for the first two. It would be better if blank questions were automatically removed. The accepts_nested_attributes_for method has a reject_if option that we can use to do this. The method accepts a lambda which has a hash of attributes passed to it and we can use that hash to reject a question if its content is blank.

/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

If we create a new survey now with only two of question fields filled in only two questions will be created and we won’t see a blank question in the list.

Blank questions are no longer shown.

What if we want to delete existing questions when we’re editing a survey? In the final application we want to use a link to delete questions but for now we’re going to take the easier option and use a checkbox. In the survey form partial we’ll add a checkbox and a label to go with it.

/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 %>

The trick here is the attribute name for the checkbox: _destroy. When this has a value of true, i.e. when the checkbox is checked, the record will be removed when the form is submitted.

In order to make this work we need to enable it in accepts_nested_attributes_for in the Survey model by adding :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

When we reload the page now we’ll have a “Remove Question” checkbox against each question.

We now have checkboxes so that we can remove questions.

And if we check the “Remove Question” checkbox against one of the questions and submit the form that question will be removed.

The question has been succesfully removed.

Adding Answers

We now have the questions set up as we want them but not the answers. We’ll start on that now by creating the Answer model and setting up the nesting. First we’ll generate the model.

script/generate model answer question_id:integer content:string

Then migrate the database again.

rake db:migrate

Next we’ll set up the relationship between Answer and Question. Answer will belong_to Question.

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

For Question we’ll need to use accepts_nested_attributes_for as we did in the 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

In the form we’ll need to add fields for answers but the form view code will become cluttered if we add another nested model to it. Later on we’ll want to add questions on the form via a link with JavaScript so for both these reasons we’ll move the form code that renders each question into a partial called question_fields.

After doing this the form view will look like this.

/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 %>

Note that we’re just passing the name of the partial as a string to render, making use of the new short form that was introduced in Rails 2.3. We also pass the builder to the partial with a name of f. In the new question_fields partial we can then use that f variable to render the form elements for a 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>

We can handle the answer fields in a similar way and put them in their own partial file. In the _question_fields partial we’ll loop through the answers for the question and render a new partial called _answer_fields.

/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>
<% f.fields_for :answers do |builder| %>
  <%= render 'answer_fields', :f => builder %>
<% end %>

In our new _answer_fields partial we’ll put the code to render an 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>

So that we can see the answer fields on the form we’ll modify the SurveyController’s new action so that the three new questions that are created each create four answers.

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

Now, when we create a survey we have three questions each with four answers.

Answer fields are now shown in the survey form.

When we fill in the fields and submit the form the answers won’t be shown but we can easily fix that. In the section of the show view for a survey that renders each question we’ll add some code to render each question’s answers.

/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>

If we reload the survey’s page now it will show the questions and the answers.

The answers are now displayed underneath their questions.

We aren’t quite done yet as we want to be able to add or remove questions or answers on the form dynamically via links. We’ll cover this in the next episode.