This post was originally published at https://jessesbyers.github.io./ on January 23, 2020, when I was a Software Engineering student at Flatiron School.
In my first post, I used a Contact List example to illustrate basic CRUD actions in Rails. In this post, I’ll build on that example to develop more complex model associations, and build a single nested form that allows users to create instances of multiple models with the correct associations. This will rely heavily on form helper methods and custom writer methods, and we’ll look at the HTML that is generated by using these methods with our forms.
Building on the Contact List Example
Imagine that the User for our Contact List app is the local fire department. In addition to knowing each household’s address information, it is important to know how many pets the family has in order to make sure the pets are rescued in case of an emergency. Our more complex app will have the following models and associations:
- A User has many Contacts and a Contact belongs to a User.
- A User has a name (string data type).
- Each Contact has a name, address, phone number, and email address (all string data types).
- A Contact has many Pets, and a Pet belongs to a Contact.
- A Pet has a name and a breed (both string data types).
- To finish off the associations, a User has many Pets, through its Contacts
To get started, I’ll generate the User model and the Pet model, update the Contact model with new associations, and create migrations for the new models. I will also need to update my seed data to account for new models and represent the more complex relationships.
rails generate resource User name:string
rails generate resource Pet name:string breed:string
Creating a Nested Form
In my previous post, our simple Contact model included a form to create contacts, but it only included attributes for the contact itself (name, address, phone, and email). Now that we have multiple models and complex relationships, we can create a nested form, which allows us to create contacts and their pets at the same time, while maintaining the correct associations between them.
In order to do this, we will follow these steps:
- Plan out what our params hash should look like to include attributes for multiple models
- Write custom writers or use macros to identify each key in the new params hash
- Use form helpers to create a form aligned to the params we want to end up with
- Inspect the form in the browser to ensure structure is correct and params is set up correctly
- Update the controller to accept and process the new params hash
1. Plan Out Params Hash
Previously, our simple new form generated a params hash that looked like this:
contact = {
:name => "Jane Smith",
:address => "123 Pine St, New York, NY 12121",
:phone => "518-872-3562",
:email => "jane@gmail.com"
}
We used this params hash to create a new contact using this code in the controller:
def create
@contact = Contact.new(contact_params(:name, :address, :phone, :email))
@contact.save
redirect_to contact_path(@contact)
end
Before we can build our new form with fields for our new contacts pets, we need to imagine what the new hash must look like. We will be creating a key called "pets_attributes" that will have a value of a hash of attributes for each pet that is created, nested within the contact key.
contact = {
:user_id => 1
:name => "Jane Smith",
:address => "123 Pine St, New York, NY 12121",
:phone => "518-872-3562",
:email => "jane@gmail.com",
:pets_attributes = {
0 => {:name => "Pepper", :breed => "Dog"}
1 => {:name => "Snowstorm", :breed => "Cat"}
2 => {:name => "Fawkes", :breed => "Bird"}
}
}
2. Write custom writers or use macros to identify each key in the new params hash
ActiveRecord has already given us reader and writer methods for most of the keys in our hash (name, address, phone, email, and pets). However, we do not have a writer method for pets_attributes. There are two ways to create this method, which will allow us to access this variable in the form and in our controller.
Macro: accepts_nested_attributes_for
The easiest, but least flexible way to do this is to use a macro at the top of the Contact model file. This will create the writer method of pets_attributes=(), and allow us to use the pets_attributes key in our params hash:
accepts_nested_attributes_for :pets
We can add to this macro to allow the form to reject any empty fields in the form:
Custom writer method: _attributes=()
The more flexible approach is to write a custom writer method in the Contact model.
This method allows us to take all of the values that are in the pets_attributes hash within params and use those values to create pets, which are then added to the current Contact’s collection of pets.
3. Use form helpers to create a form aligned to the params we want to end up with
Now we are ready to create a complex form that nests values for our pets within our contacts form. This relies heavily on form helpers, which are really just Ruby methods that transform Ruby code into HTML.
form_for
The form_for tag automatically detects whether the form should submit a GET or POST request, and can automatically route the request accordingly, as well as set the authenticity token. The “f” represents the object, and allows us to generate fields based on each of the object's attributes.
fields_for
The fields_for tag is similar to the form_for tag, but we can use it to nest a belongs_to model (pets) within the model that it belongs to (contact).
Together, the field names that are generated within these two form tags should match the keys on our params hash that we planned out, if everything is structured correctly.
4. Inspect the form in the browser.
By inspecting the form in browser, we can see how the form helpers were able to automatically generate the HTML we needed for our form.
This snippet includes the HTML for a dropdown menu to choose the User the Contact belongs to, generated by using the collection_select helper method.
This snippet shows the contact fields that were automatically generated by the form_for helper method.
This snippet shows the pets attributes fields that were generated by the fields_for helper method. Notice the naming conventions of each field, as well as the datalist section which allows the user to choose an existing pet breed, or create their own.
5. Update the controller to accept and process the new params hash
There are a few areas in the controller which need to be updated in order to properly create a new contact with associated pets. The new method in the controller needs to be updated to create a few placeholder pets, using the .build method. By creating a certain number of placeholders here, it will automatically generate that many pet fields in the form.
Next, you will need to re-visit which params will be allowed in the hash to create a new Contact through mass assignment. The simplest way to do this is to update the arguments within the private contact_params method.
End Result
After following these steps, we are able to create a nested form, and update our controller and model files in order to access the nested parameters to create instances of multiple models, while maintaining the correct associations among them. Now we can rest easy knowing that all of our pets will be saved in the Fire Department's database, and will be protected in case of emergency!
How To Series
In this series of How To posts, I will be summarizing the key points of essential topics and illustrating them with a simple example. I’ll briefly explain what each piece of code does and how it works. Stay tuned as I add more How To posts in the series each week!
Top comments (3)
This was really useful Jesse! Thanks for taking the time to write it all out.
Glad this was helpful! I wrote this because I REALLY struggled to understand nested forms when I first encountered them, so writing this really helped me to understand how it all comes together.
Thank you for this. I've been searching all day.