Differences

This shows you the differences between two versions of the page.

Link to this comparison view

nested_model_form_with_ruby_on_rails_4_en [2017/09/05 12:18] (current)
Line 1: Line 1:
 +====== 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.
 +<sxh bash>
 +rails new surveysays
 +</​sxh>​
 +
 +Now let's access the new directory ​
 +<sxh bash>
 +cd surveysays/
 +</​sxh>​
 +
 +Now let's crated the scaffold for the survey that will be the base of the application.
 +<sxh bash>
 +rails generate scaffold survey name:string
 +</​sxh>​
 +
 +Now let's run the migration
 +<sxh bash>
 +rake db:migrate
 +</​sxh>​
 +
 +Now let's run rails server
 +<sxh bash>
 +rails server -b 0.0.0.0 -p 3000
 +</​sxh>​
 +
 +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
 +<sxh bash>
 +rails generate model question survey_id:​integer content:​text
 +</​sxh>​
 +
 +Now let's create the answer model
 +<sxh bash>
 +rails generate model answer question_id:​integer content:​string
 +</​sxh>​
 +
 +Now let's run the migration
 +<sxh bash>
 +rake db:migrate
 +</​sxh>​
 +
 +Now we need to create the relationship between the models
 +
 +Let's change the Survey Model
 +<sxh ruby>
 +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
 +</​sxh>​
 +
 +Let's change the Question Model
 +<sxh ruby>
 +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
 +</​sxh>​
 +
 +Let's change the Answer Model
 +<sxh ruby>
 +vim app/​models/​answer.rb
 +class Answer < ActiveRecord::​Base
 +  # Each answer belongs to a question
 +  belongs_to :question
 +end
 +</​sxh>​
 +
 +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.
 +<sxh ruby>
 +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
 +</​sxh>​
 +
 +Now we need to configure the form of the Survey ​
 +<sxh xml>
 +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 %>
 +</​sxh>​
 +
 +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
 +<sxh xml>
 +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 %>
 +</​sxh>​
 +
 +Now we need to create another partial that will store the information about the fields that needs to be showed when render the question
 +<sxh xml>
 +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>
 +</​sxh>​
 +
 +Now we need to change the show view to show the information about the question and the answer that belongs to the Survey.
 +<sxh xml>
 +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>
 +
 +</​sxh>​
 +
 +Now we can run the rails server
 +<sxh bash>
 +rails server -b 0.0.0.0 -p 3000
 +</​sxh>​
 +
 +Now we can test the Survey Form in: http://​ip_machine:​3000/​surveys and click in New Survey.
 +
 +We will get something like
 +
 +{{::​nested_form_01.png?​300|}}
 +
 +After create the first Survey we will get something like
 +
 +{{::​nested_form_02.png?​300|}}
 +
 +If you try to edit the form we will get something like
 +
 +{{::​nested_form_03.png?​300|}}
 +
 +Now we can check the information about the survey with the rails console
 +<sxh bash>
 +rails console
 +</​sxh>​
 +
 +Getting all the Survey
 +<sxh ruby>
 +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">​]>​
 +</​sxh>​
 +
 +Now getting all the question
 +<sxh ruby>
 +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">​]>​
 +</​sxh>​
 +
 +Now getting all the answers
 +<sxh ruby>
 +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">​]>​
 +</​sxh>​
 +
 +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
 +<sxh xml>
 +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 %>
 +</​sxh>​
 +
 +Now we need to change the answer fields partial
 +<sxh xml>
 +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>
 +</​sxh>​
 +
 +Now we need to change the question fields partial
 +<sxh xml>
 +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>​
 +</​sxh>​
 +
 +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
 +<sxh ruby>
 +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
 +</​sxh>​
 +
 +After created the helper methods we need to create the jQuery functions to handle the new options
 +<sxh js>
 +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));
 +}
 +</​sxh>​
 +
 +Now we can add and remove answers and questions from the form only clicking on remove or Add Answer or Add Question.
 +
 +{{::​nested_form_04.png?​300|}}
 +
 +The same behavior is applies to Edit option.
 +
 +{{::​nested_form_05.png?​300|}}
 +====== References ======
 +  - http://​railscasts.com/​episodes/​196-nested-model-form-part-1
 +  - http://​railscasts.com/​episodes/​197-nested-model-form-part-2
 +  - http://​api.rubyonrails.org/​
 +  - http://​stackoverflow.com/​questions/​15919761/​rails-4-nested-attributes-unpermitted-parameters
 +  - http://​railscasts.com/​episodes/​197-nested-model-form-part-2?​view=comments
 +