Nested Model Form with Ruby on Rails 4

Handling multiple models in a single form is much easier with the accepts_nested_attributes_for method. See how to use this method to handle nested model fields.

Let's created the application to apply the nested model form from scratch.

rails new surveysays

Now let's access the new directory

cd surveysays/

Now let's crated the scaffold for the survey that will be the base of the application.

rails generate scaffold survey name:string

Now let's run the migration

rake db:migrate

Now let's run rails server

rails server -b 0.0.0.0 -p 3000

Now we can test in http://ip_machine:3000/surveys the beginning of the new application is started so, let's create the question model

rails generate model question survey_id:integer content:text

Now let's create the answer model

rails generate model answer question_id:integer content:string

Now let's run the migration

rake db:migrate

Now we need to create the relationship between the models

Let's change the Survey Model

vim app/models/survey.rb
class Survey < ActiveRecord::Base
  # Survey can have many questions and when delete a survey
  # the question that belongs to the specific survey will be deleted as well. 
  has_many :questions, :dependent => :destroy
  # Enable to work with nested attributes, so we can work with attributes from questions
  # the attributes that are empty will not be inject into the database.
  # Destroy any attributes given
  accepts_nested_attributes_for :questions, reject_if: proc { |attributes| attributes['content'].blank? }, allow_destroy: true
end

Let's change the Question Model

vim app/models/question.rb
class Question < ActiveRecord::Base
  # Each Survey belongs to a survey
  belongs_to :survey
  # Each Question has many answers, and when delete a question will delete the answer
  has_many :answers, :dependent => :destroy
  # Enable to work with nested attributes, so we can work with attributes from answers
  # the attributes that are empty will not be inject into the database.
  # Destroy any attributes given
  accepts_nested_attributes_for :answers, reject_if: proc { |attributes| attributes['content'].blank? }, allow_destroy: true
end

Let's change the Answer Model

vim app/models/answer.rb
class Answer < ActiveRecord::Base
  # Each answer belongs to a question
  belongs_to :question
end

Now we need to change the Surveys Controller, here we will create the structure that will be showed to the user and we will following a pattern for each new survey request we will give some default form fields, and we need to configure the strong parameters to accept the nested parameters.

vim app/controllers/surveys_controller.rb
[...]
  def new
    @survey = Survey.new
    # Creating 3 question for each Survey
    3.times do
     question = @survey.questions.build 
     # Creating 4 answers for each question.
     4.times { question.answers.build }
    end
  end
[...]
  def survey_params
    # We need to get the question_attributes that will send from the same form.
    # and inside the question_attributes we will have the answers_attributes that needs
    # to be allowed too.
    params.require(:survey).permit(:name,questions_attributes:[:id, :content, :_destroy, answers_attributes: [:id, :content, :_destroy]])
  end

Now we need to configure the form of the Survey

vim app/views/surveys/_form.html.erb
<%= form_for(@survey) do |f| %>
  <% if @survey.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@survey.errors.count, "error") %> prohibited this survey from being saved:</h2>

      <ul>
      <% @survey.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :questions do |builder| %>
    <%= render "question_fields", :f => builder %>
  <% end %>


  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Now we need to create the question partial that will store the information about the fields that needs to be showed when render the Survey

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

Now we need to create another partial that will store the information about the fields that needs to be showed when render the question

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

Now we need to change the show view to show the information about the question and the answer that belongs to the Survey.

vim app/views/surveys/show.html.erb
<p id="notice"><%= notice %></p>

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

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

<%= link_to 'Edit', edit_survey_path(@survey) %> |
<%= link_to 'Back', surveys_path %>
<br>

Now we can run the rails server

rails server -b 0.0.0.0 -p 3000

Now we can test the Survey Form in: http://ip_machine:3000/surveys and click in New Survey.

We will get something like

After create the first Survey we will get something like

If you try to edit the form we will get something like

Now we can check the information about the survey with the rails console

rails console

Getting all the Survey

irb(main):001:0> Survey.all
  Survey Load (1.7ms)  SELECT "surveys".* FROM "surveys"
=> #<ActiveRecord::Relation [#<Survey id: 2, name: "Rails Survey", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">]>

Now getting all the question

irb(main):002:0> Question.all
  Question Load (0.4ms)  SELECT "questions".* FROM "questions"
=> #<ActiveRecord::Relation [#<Question id: 2, survey_id: 2, content: "How Many application do you have in production?", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Question id: 3, survey_id: 2, content: "Which JavaScript framework do you prefer?", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">]>

Now getting all the answers

irb(main):003:0> Answer.all
  Answer Load (0.2ms)  SELECT "answers".* FROM "answers"
=> #<ActiveRecord::Relation [#<Answer id: 4, question_id: 2, content: "one", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Answer id: 5, question_id: 2, content: "two", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Answer id: 6, question_id: 2, content: "three", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Answer id: 7, question_id: 2, content: "None", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Answer id: 8, question_id: 3, content: "Prototype", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">, #<Answer id: 9, question_id: 3, content: "jQuery", created_at: "2016-02-11 19:08:40", updated_at: "2016-02-11 19:08:40">]>

So far we have all the information we need, but the form is far from the interactive that we need.

Now let's change the form to add a link to add new partials for the questions and the answers and let's created a jQuery function to handle remove and add new partials inside the Survey Form

Let's change the Form of Survey

vim app/views/surveys/_form.html.erb
<%= form_for(@survey) do |f| %>
  <% if @survey.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@survey.errors.count, "error") %> prohibited this survey from being saved:</h2>

      <ul>
      <% @survey.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :questions do |builder| %>
    <%= render 'question_fields', :f => builder  %>
  <% end %>
  
  <p><%= link_to_add_fields "Add Question", f, :questions %></p>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Now we need to change the answer fields partial

vim app/views/surveys/_answer_fields.html.erb 
<p class="fields">
 <%= f.label :content, "Answer" %>
 <%= f.text_field :content%>
 <%= link_to_remove_fields "remove", f %>
</p>

Now we need to change the question fields partial

vim app/views/surveys/_question_fields.html.erb
<div class="fields">
  <p>
   <%= f.label :content, "Question" %>
   <%= link_to_remove_fields "remove", f %><br>
   <%= f.text_area :content, :rows => 3 %>
  </p>

  <%= f.fields_for :answers do |builder| %>
    <%= render 'answer_fields', :f => builder %>
  <% end %>
  <p><%= link_to_add_fields "Add Answer", f, :answers %></p>
</div>

We added new options to our form, but them are now working yet, we need to create the new methods to handle add and remove fields from the form

vim app/helpers/application_helper.rb
module ApplicationHelper


        # Method to handle remove fields form the form
  def link_to_remove_fields(name, f)
    f.hidden_field(:_destroy) + link_to("remove", '#', onclick: 'remove_fields(this)')
  end


        # Method to handle the add fields to the form
  def link_to_add_fields(name, f, association)
    new_object = f.object.class.reflect_on_association(association).klass.new
    fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
      render(association.to_s.singularize + "_fields", :f => builder)
    end
     link_to(name, "#", "data-association" => "#{association}" , "data-content" => "#{fields}", :class => "link_to_add_fields" )
  end

end

After created the helper methods we need to create the jQuery functions to handle the new options

vim app/assets/javascripts/application.js 
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .

function remove_fields(link) {
  $(link).prev("input[type=hidden]").val("1");
  $(link).closest(".fields").hide();
}

$(document).on("click", "a.link_to_add_fields", function(e){
    e.preventDefault();
    var link = $(this);
    var association = $(this).data("association");
    var content = $(this).data("content");
    add_fields(link, association, content);
});

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));
}

Now we can add and remove answers and questions from the form only clicking on remove or Add Answer or Add Question.

The same behavior is applies to Edit option.

References