196: Nested Model Form Part 1
(view original Railscast)
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.
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.
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.
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.
/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.
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.
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.
And if we check the “Remove Question” checkbox against one of the questions and submit the form that question will be 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.
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.
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.