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:
<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 relationshipreject_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 blanklimit: X
whereX
is the maximum amount of element allowed in a one-to-many relationshipupdate_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 noid
, 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 thepets_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 withupdate_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.