Nested forms with Rails 4.2 Strong Parameters

Rails 4 strong parameters change the way developers have to go about forms for nested has_many relationships. This has caused many developers to rely on gems, like cocoon, that can make nested forms a little easier. Such gems are great, but in many cases setup is simple enough without a gem and can help a developer gain a better understanding of the Rails framework.

Here I’ll talk about how I updated the approach taken in RailsCast #196 (revised), in which Ryan Bates sets up a doubly-nested form to create surveys, add questions to it, and add answer options to those questions. In this scenario a survey has_many questions (each question belongs_to a survey), and each question has_many answers (each answer belongs_to a question), and we want a survey edit form that can update nested attributes for questions and their answers. I deviate in the model setup from Ryan’s approach, so this will work with Rails 4.2 strong parameters. The same approach could be used for any nested form for a simple has_many:belongs_to association.

For this activity, there are a three questions we need to answer. The answers can be elusive but instructive to work thorugh.

  1. How do we set up models so that attributes from nested relationships can be included in a single form?
  2. How does the parent model instance know which child model records it owns (i.e., which questions are associated with it, and in turn which answers are associated with those questions)?
  3. How do we set up strong parameters, given that the survey controller will be responsible for rendering the form that will service all three models?

When you see how these things are set up, a little about how Rails works is revealed.

Herein, I’ll go a little further and also show how easy it is to add/remove nested form fields dynamically via some Coffeescript. Given what all of this accomplishes, the number of lines of code a developer actually has to write is remarkably small, but there are definitely a few “moving parts” involved, so lets dig in…

Model setup

The basic setup of the models is seen here.

app/models/survey.rb
1
2
3
4
5
6
7
8
9
class Survey < ActiveRecord::Base
  has_many :questions
  accepts_nested_attributes_for :questions, allow_destroy: true

  def questions_for_form
    collection = questions.where(survey_id: id)
    collection.any? ? collection : questions.build
  end
end
app/models/question.rb
1
2
3
4
5
6
7
8
9
10
class Question < ActiveRecord::Base
  belongs_to :survey
  has_many :answers
  accepts_nested_attributes_for :answers, allow_destroy: true

  def answers_for_form
    collection = answers.where(survey_id: id)
    collection.any? ? collection : answers.build
  end
end
app/models/answer.rb
1
2
3
class Answer < ActiveRecord::Base
  belongs_to :question
end

Pay special attention to how the has_many and belongs_to relationships among the models, which defines the how the model relationships are “nested.” In addition to this, there are two things here that are required for the forms helper to create a working nested form.

First, the accepts_nested_attributes_for method allows each parent model to accept nested attributes for it’s respected child model. This enables you to use a nested fields_for block inside the form_for block on the new Survey form (for questions), and another fields_for block within the questions sub-form (for answers). This is the answer to our first question above. By setting allow_destroy: true, we also delegate authority to the Survey model to remove related questions and answers from surveys and questions, respectively.

Second, the methods in the Survey and Question model classes that return a collection of the related records from their child models (Question and Answer, respectively). This rounds out the basic model requirements for setting up a nested form with this type of model relationship. This answers the second question above.

The Controller

Read through and understand what each of the actions in the Surveys controller do. Each one retrieves an instance of an object or a collection, and provides it to the view that it renders. Because our models are nested and set properly, the related records from the child models will also be available to instances of Survey, unless they don’t yet exist. In that case we just need to “build” them (see the new method).

That’s all well and good, but our final questions at the top of the article is answered in this controller as well, and that is how strong parameters should be set up (the survey_params method).

app/controllers/surveys_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class SurveysController < ApplicationController
  def index
    @surveys = Survey.all
  end

  def show
    @survey = Survey.find(params[:id])
  end

  def new
    @survey = Survey.new
    @survey.questions.build
  end

  def create
    @survey = Survey.new(survey_params)
    if @survey.save
      redirect_to @survey, notice: 'Survey was successfully created.'
    else
      render :new
    end
  end

  def edit
    @survey = Survey.find(params[:id])
  end

  def update
    @survey = Survey.find(params[:id])
    if @survey.update(survey_params)
      redirect_to @survey, notice: 'Survey was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @survey = Survey.find(params[:id])
    @survey.destroy
      redirect_to surveys_url, notice: 'Survey was successfully destroyed.'
  end

  private
    def survey_params
      params.require(:survey).permit(:title, questions_attributes: [ :id, :text, :_destroy, answers_attributes: [ :id, :content, :_destroy ]])
    end
end

In Rails 4, the require and permit methods determine which parameters are “allowed” to be created or updated via post, and put requests. This is to prevent someone from performing a direct POST or PUT to exercise direct control over attributes you’d prefer not to be user editable (see inset panel below). If we want to permit our Survey controller’s create and update actions to save related nested question and answer record attributes, these have to be added to our strong parameters .permit() method, and nested in the same way the models are nested. That is, the hash of permitted question attributes is passed among the survey attributes to the .permit() method, and nested within the questions has, is the hash of permitted answers.

params.require(:survey).permit(:title, questions_attributes: [ :id, :text, :_destroy, answers_attributes: [ :id, :content, :_destroy ]])

This is probably the trickiest idea here, that the strong parameters for all three models have to be set up in the parent controller in order for any attributes of the child/nested models to be saved. If you think about it, however, it makes sense. We’ve delegated the save, updated, and destroy actions for the nested models to the Survey. If this is all our application will do… Create surveys from a single form, we don’t even need actions in questions or answers controller at all. We just need the models to delegate properly to the parent model class. I think that’s simplified things a lot.

Strong Parameters

It’s difficult to see how allowing a hacker to associate change the association of a question to a different survey would be a big disaster, but there are other cases in which preventing this kind of attack would be more important. Imagine someone made a put request with a REST client application to turn on the “admin” flag on their user account, for example.

The routes

Of course, I don’t want to forget the routes.

config/routes.rb
1
2
3
4
5
6
Rails.application.routes.draw do
  root "surveys#index"
  resources :answers
  resources :questions
  resources :surveys
end

The form partials

We still need to set up our form, also. I did this using a survey form partial where my form_for lives for the Survey, which includes a question field partial with the nested fields_for for my Questions, and which then includes an answers field partial with the nested fields_for for my Answers (see below). While you look through the form partials, you may notice that there is an unfamiliar method called in the erb, link_to_add_fields. We’ll have to add this helper method and some nifty Coffeescript to allow for slick dynamic add/remove functionality for questions and answers.

app/views/surveys/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%= 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 :title %><br>
    <%= f.text_field :title %>
  </div>

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

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
app/views/surveys/_question_fields.html.erb
1
2
3
4
5
6
7
8
9
10
11
<fieldset>
  <%= f.label :text, "Question" %>
  <%= f.text_area :text %>
  <%= f.hidden_field :_destroy %>
  <%= link_to "remove", '#', class: "remove_fields" %>

  <%= f.fields_for :answers do |answers_for_form| %>
    <%= render 'answer_fields', f: answers_for_form %>
  <% end %>
  <%= link_to_add_fields "Add answer", f, :answers %>
</fieldset>
app/views/surveys/_answer_fields.html.erb
1
2
3
4
5
6
<fieldset>
  <%= f.label :content, "Answer" %>
  <%= f.text_area :content %>
  <%= f.hidden_field :_destroy %>
  <%= link_to "remove", '#', class: "remove_fields" %>
</fieldset>

by storing the :_destroy attribute in a hidden field, and adding the remove link in the question and answer form partials sets us up for using javascript later to add/remove fields dynamically from the form. The other option is to use a checkbox given the :_destroy attribute <%= f.check_box :_destroy %> instead (also adding a label, so you know what the checkbox is for, but this leads to the need to check a box, and submit the form to reload it to show the field removed. I think the javascript approach is cooler, so I’ll stick with that.

Adding and Removing Fields

So, as I said, rather than having to reload the page to add/remove questions and answers to surveys, we’ve set up our partials to dynamically add/remove fields using javascript instead. Now, we need to add the javascript needed to handle the field removals, and a helper function and javascript to handle addition of fields.

First, for the javascript, which we’ll enter as Coffeescript. The first function, to remove fields will compile to the javascript needed to remove either questions or answers when clicking the corresponding remove link. This should work as-is. The second function to add fields will require a helper function to work.

app/assets/javascripts/surveys.coffee
1
2
3
4
5
6
7
8
9
10
11
jQuery ->
  $(document).on 'click', '.remove_fields', (event) ->
    $(this).prev('input[type=hidden]').val('1')
    $(this).closest('fieldset').hide()
    event.preventDefault()

  $(document).on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

The helper function I’ve been alluding to above follows. We’ll pass in the the name of our link (“Add Answer”), an f variable (the form builder object for this question), and the association (either :questions or :answers). The helper method uses these to build a new instance of the association record, then grabs the object_id. Then, it calls fields_for on the form builder object, and passes it the association, the new object instance, and the new object id as the :child_index, before rendering the appropriate form partial for the association, and then a new “add_fields” link. If you really want to understand this, I recommend watching Ryan Bates’ Railscast #196 (revised), starting at about 7:00 in, and if you’re a Railscasts subscriber, you can also view the notes, which also include some explanation on this helper method. ##### app/helpers/survey_helper.rb

1
2
3
4
5
6
7
8
9
10
module SurveyHelper
  def link_to_add_fields(name, f, association)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id
    fields = f.fields_for(association, new_object, child_index: id) do |questions_for_form|
      render(association.to_s.singularize + "_fields", f: questions_for_form)
    end
    link_to(name, '', class: "add_fields", data: { id: id, fields: fields.gsub("\n", "")})
  end
end

If you just follow the Railscast, but you’re using Rails 4.2, you may be frustrated that the form just doesn’t update properly with changes, and if you check your logs you’ll likely see that it’s due to the strong parameter setup being incorrect for the current Rails version. If you set up your model like I have here, however, you should have a fully operational nested form.

Cheers!

Created 3/19/2015 1:17PM (MDT) | Last Updated 3/23/2015 6:19PM (MDT)

Comments

Claudiney Veloso Claudiney Veloso 5/23/2015 5:45PM (MDT)

Hello Mark, I’m using his example in a rails application … but I’m finding the following problem. Once you click on the link “link_to_add_fields” my partial is executed several times. I checked that this happens every to click on the my app: For example, if I click on other application links, and after click on the link “link_to_add_fields” it returns the number of previous clicks… will be that you can help me solve this error?

Mark Coleman Mark Coleman 6/15/2015 6:07PM (MDT)

There are a couple of reasons I’ve seen for this. One is multiple redundant inclusions of the jquery code, or the jquery library itself. If you’re starting with a freshly created rails 4+ app. You’ll have to pursue that possibility yourself, as I’ve no direct experience with this problem (though I’ve known others who had inadvertently gotten a second include of jQuery when they upgraded an app to rails 4+. You may be able to learn whether this is an issue for you by using browser developer tools to see if there is more than one inclusion of jQuery.

The other that I have personal experience with is malformed HTML in the page view. If you are using HAML for your views, it’s easy to indent improperly. For example, if you don’t indent everything under the html tag, HAML compiles to open and close the HTML tag before the head and body of the document. The closing HTML tag tells javascript your page has loaded (before it actually has in, in this case). I’m not sure of the exact mechanism, but for me in two small apps, this resulted in ajax/coffeescript methods to add input fields working multiple times. This issue is easy enough to check by just validating the HTML of your page, and easy to fix with a text editor and the tab key.

I’m sure there are other causes, but from my experience these are the most likely.

Christina Christina 4/13/2016 4:30PM (MDT)

Hello, I have been trying to add a nested form to the app from Hartl’s Rails Tutorial. However, it does not appear to be working for me. Would you mind taking a look at my question submitted on Stack Overflow (http://stackoverflow.com/questions/36610500/nested-form-not-displaying-submitted-content-on-show-page) to see if you or anyone might know the source of the problem? Thank you very much!!

Mark Coleman Mark Coleman 4/19/2016 7:46AM (MDT)

I had a look. Learning the ins and outs of instance vs. local variables is one good reason to learn to do these things without a gem at first. I found there were gems for many things out there, but they are sort of like a black box. They abstract away a lot of things about the framework when digging into those things can help you learn. After learning these things experientially, you can choose a gem not because it magically produces the result you want and don’t know how to achieve on your own, but because you know what it does, and it makes your project easier.

Log in to add comments.