Nested Forms in Rails

Have you ever had to deal with complex forms creating multiple objects and hierarchies in one request? Rails is there to help provide a set of helpers, methods and conventions to build nested forms, handle assignment, and creation of the objects involved in only a few lines of code. In this blog I'll explain how that works using accepts_nested_attributes_for, fields_for, strong parameters and more.

The Convention

Let's start with the parameters Rails expects in the request. Suppose we have this object and relationships: a Person has one Address and has many Pets.

We would have these models:

class Person < ApplicationRecord
  has_one :address
  has_many :pets
end

class Address < ApplicationRecord
  belongs_to :person
end

class Pet < ApplicationRecord
  belongs_to :person
end

Rails will expect the params hash for a Person to be something like this:

{
  person: {
    address_attributes: {
      street: '',
      number: ''
    }
    pets_attributes: {
      1: { # this does not need to match the id, it only needs to be uniq within this hash
        id: 1,
        name: '',
        breed: ''
        :_destroy => 1 # this is optional, we'll talk about it later
      },
      2: {
        id: 2,
        name: '',
        breed: '',
      },
      3: {
        # no id for new elements to be created
        name: '',
        breed: ''
      }
    }
  }
}

So, it requires the main object's class as a wrapper key. Then, for one-to-one relationships it requires a key #{singular_underscored_class_name}_attributes key wrapping all the attributes. And, for one-to-many relationships, it requires a {plural_underscored_class_name}_attributes key wrapping each of the related objects, and it can include or not an id key and a _destroy key depending on what we want to do with that specific element.

For the inputs' names, that convention will translate to something like: html <input name="person[pets_attributes][1][id]" /> <input name="person[pets_attributes][1][name]" /> <input name="person[pets_attributes][1][_destroy]" /> <input name="person[address_attributes][street]" />

fields_for

We know what ActiveRecord expects for the params hash, but we don't have to remember that convention nor write the input's name manually. Rails provides a fields_for helper method that will take care of creating the right loops and names to adhere to that convention.

Continuing with the example, this is the basic idea to use it:

form_for @person do |form|
  # some fields for the @person object like:
  form.text_field :name

  form.fields_for :address do |address_subform|
    # fields for the associated address object
    address_subform.text_field :street
    address_subform.text_field :number
  end

  form.fields_for :pets do |pet_subform|
    # this will run once for each of the pets
    pet_subform.text_field :name
    pet_subform.text_field :breed
  end

  form.submit
end

I wrote it like pure Ruby code to make it cleaner, but you will probably use it inside a view with some markup to differentiate the sections, add styles, etc.

fields_for will check the current association to know which object/s to use, so in your new action you can build a new record not only for your main object but also the associated ones like this:

@person = Person.new
@person.build_address
@person.pets.build

This way, fields_for will have specific elements to use.

If you try that code as is, it won't work yet. fields_for will do some magic behind the scenes and it won't use the right conventions unless you configure your Person model properly using the accepts_nested_attributes_for class method.

accepts_nested_attributes_for

ActiveRecord includes the NestedAttributes Module that takes care of setting the attributes for the associated objects. Let's continue with the example: to add support for this we need to use the accepts_nested_attributes_for class method on our object.

class Person < ApplicationRecord
  has_one :address
  has_many :pets
  accepts_nested_attributes_for :address, :pets
  # you can split the line above into 2 `accepts_nested_attributes_for`
  # lines, one for each association, to configure them differently
end

This will add a few methods for our Person objects, the most important for us will be address_attributes= and pets_attributes=. Notice the methods match the keys that I mentioned before in the Convention section.

ActiveRecord is smart enough to know that one of the associations is a one-to-one and the other association is a one-to-many from the associations already configured, we don't need to specify anything else.

accepts_nested_attributes_for supports many options to configure how the *_attributes= methods work:

  • allow_destroy: true will enable the use of the optional _destroy key if we want to tell Rails to remove one object from a one-to-many relationship
  • reject_if: ... accepts a block that receives each group of attributes for each object on a one-to-many relationship and we can query the hash to remove it from the list. It also accepts :all_blank that internally creates a proc that checks if all values are blank
  • limit: X where X is the maximum amount of element allowed in a one-to-many relationship
  • update_only helps you if you need a one-to-one relationship and you don't want to expose the id of the child object. If there's no id, Rails will create a new object by default, but, if this option is set to true, Rails will always update the current child if present

Combining fields_for with accepts_nested_attributes_for

In the previous section we saw many options, some of them only apply for the setter but a few of them require some consideration when building the form.

allow_destroy: true

In order for a user to be able to remove elements from a one-to-many relationship, we can provide an input element with the key _destroy. The most basic example would be to do:

class Person < ApplicationRecord
  has_many :pets
  accepts_nested_attributes_for :pets, allow_destroy: true
end
  form.fields_for :pets do |pet_subform|
    # this will run once for each of the pets
    pet_subform.text_field :name
    pet_subform.text_field :breed
    pet_subform.check_box :_destroy
  end

Now a user can check that input to delete the associated object.

You can use any frontend interaction (maybe an X button?) as long as you set the value of the _destroy key to a truthy value included in [1, "1", "true", true].

update_only: true

By default, if the associated elements have no id key, Rails will consider them as new objects to be created. If you already have an associated persisted object and you omit the id key in your form (as a hidden field), Rails will override that object with a new one with the same attributes because of this.

You can prevent that for one-to-one associations (you may want to not expose the associated element's id for security reasons) enabling that like this:

class Person < ApplicationRecord
  has_one :address
  accepts_nested_attributes_for :address, update_only: true
end
  form.fields_for :address, include_id: false do |address_subform|
    # fields for the associated address object
  end

Strong Parameters

We talked about the convention and the Rails helpers used for the models and the views, but we need to talk about the controllers and the security too.

Now that we have the form submitting the expected parameters hash, we need to tell Rails how to use it, and how to do that safely. This pattern relies on mass-assignment, which can be unsafe if not done with care, so we will use StrongParameters with some special keys.

Let's use the example associations to make it more clear. For the create action of a Person, we have something like:

def create
  person = Person.new person_params_for_create
  if person.save
    # redirect or render
  else
    # show the errors
  end
end

private
def person_params_for_create
  params
    .require(:person)
    .permit(:name,
            address_attributes: [:street, :number], # permit one-to-one fields
            pets_attributes: [:id, :name, :breed, :_destroy]) # permit one-to-many fields
  end
end

We can see a few interesting things here:

  • The attributes for the address are permitted with the address_attributes key (singular for one-to-one), and the attributes for the pets are permitted with the pets_attributes key (plural for one-to-many). These are the keys we expect from the convention.
  • It doesn't need to specify anything for keys of params[:person][:pets_attributes], it only cares about the attributes
  • The pets_attributes include the :_destroy key, you can skip that one if you are not allowing destroying the records (if you permit it but your model does not allow destroying, it will do nothing)
  • For the one-to-one relationship we are not permitting the :id attribute. That works along with update_only: true options from the previous section. If you are not using that option you must add :id so Rails don't create a new record each time

Dynamic nested forms

This topic is a bit more advanced and complex, and would require a complete blog post to fully explain it. You can check this RailsCast, that, even if it's pretty old, it's still the same idea.

For some use cases we may want to have an unknown number of children for a one-to-many association. To improve the user experience, we can add and remove the elements without reloading the page using JavaScript and some tricks.

The basic idea is that we can rely on how the convention works to add more fields to the form using JavaScript, use a random number as the hash key to group attributes for a given element and let Rails do its magic.

pets_attributes: {
  1: {
    id: 1,
    name: '',
    breed: ''
  },
  'a_random_number' => {
    name: '',
    breed: '',
  },
  'another_random_number' => {
    name: '',
    breed: ''
  }
}

To simplify this task (it's quite complex), you can use a gem like Cocoon, or, if you prefer a non-jQuery alternative, I built one (inspired by Cocoon) called vanilla_nested. Both gems provide helper methods and conventions to organize your views and the required JavaScript code to make it work with little effort.

Conclusion

Thanks to the power of Rails' conventions and helper methods, it's really easy and clean to create complex objects hierarchies. Each helper takes care of some specific tasks and each part of the process includes something to assist the developer. It creates a good experience both for the developer and the user of the application.