Mass Assignment Railscast Nested
Learn more on ruby with our tutorial Understanding Eigenclasses in Ruby and Realizing Their Importance on SitePoint.
Rails provides a powerful mechanism for easily creating rich forms called “nested attributes.” It allows you to combine more than one model in your forms while keeping the same basic code pattern that you use with simple single model forms.
In this article I’ll be showing a number of different ways to use this technique. I’ll assume that you’re familiar with basic Rails forms, of the kind that are generated by the scaffolding commands. We’ll be building up a complex form step by step that allows a user to edit their preferences. Our domain is a not-for-profit management system, where volunteers (users) have areas of expertise and tasks that have been assigned to them.
The Base Form
Let’s start with a basic form that can edit a user. I assume you are familiar with this pattern, so I won’t explain it. I only present it here because the rest of the article will be building on top of this code.
First up is a simple user model with just one attribute:
We will be using the same controller for the entire of this article. This is the beauty of nested attributes—we don’t have to change our controller code!
Our base form is exactly what is generated by the Rails scaffolding:
With that out of the way, let’s dive in!
Adding an Address
We are storing a user’s address record in a separate model, but we want to be able to edit the address on the same form as other attributes of the user.
Note the addition of to the model. This method allows you to modify instances of using the same mass-assignment facility on User that makes simple forms so trivial. adds a writer method to the model that allows you to write code like this:
You can see how we won’t have to modify our controller code if we set up our form correctly, since to edit the address attributes you use the same method as you do to edit the user’s email.
To create the form, we will use the builder method. This is a complicated method that can do many things. Rather than explain it all upfront, I will introduce some of its many behaviours through the upcoming examples.
First of all you can pass a symbol of a relationship name and it will intuit from that relationship how it should render the fields. I know that sounds complicated, but the following code snippet should make it clearer:
Please note the changed variable name for the block— rather than . In this case for a relationship, the logic is “if an address exists, show a field to edit the city attribute. Otherwise if there is no address, don’t show any fields.” Here we hit our first stumbling block: if the fields are hidden when there is no address, how do we create an address record in the first place? Since this is a view problem (do we display fields or not?), we want to solve this problem in the view layer. We do this by setting up default values for the form object in a helper:
Now if the user doesn’t have an address we create a new unsaved one that will be persisted when the form is submitted. Of course, if they do have address no action is needed ( means “assign this value unless it already has a value”).
Try this out and you’ll see that rails even correctly accumulates and displays errors for the child object. It is pretty neat.
A user can have many tasks assigned to them. For this example, a task simply has a name.
There are two new options here: allow_destroy and reject_if. I’ll explain them a bit later on when they become relevant.
As with the address, we want tasks to be assigned on the same form as editing the user. We have just set up , and there are two steps remaining: adding correct inputs, and setting up default values.
When is given the name of a has many relationship, it iterates over every object in that collection and outputs the given fields once for each record. So for a user that has two tasks, the above code will create two text fields, one for each task.
In addition, for each task that is persisted in the database, a check box is created that maps to the attribute. This is a special attribute that is added by the allow_destroy option. When it is set to true, the record will be deleted rather than edited. This behaviour is disabled by default, so remember to explicitly enable it if you need it.
Note that the id of any persisted records is automatically added in a hidden field by , you don’t have to do this yourself (though if you do have to for whatever reason, is smart enough to not add it again.) View the source on the generated HTML to see for yourself.
The form we have created will allow us to edit and delete existing tasks for the user, but there is currently no way to add new tasks since for a new user with no tasks, will see an empty relationship and render no fields. As above, we fix this by adding new default tasks to the user in the view.
will iterate over these three records and create inputs for them. Now no matter how few or many tasks a user has, there will always be three blank text fields for new tasks to be added. There is a problem here though—if a blank task is submitted, is it a new record that is invalid (blank name) and should cause the save to fail, or was it never filled in? By default Rails assumes the former, but that is often not what is desired. This behaviour can be customized by specifying the reject_if option to . You can pass a lambda that is evaluated for each attributes hash, returning true if it should be rejected, or you can use the shortcut like we have above, which is equivalent to:
More complicated relationships
For this application, we want users to specify which areas in our not-for-profit they are interested in helping out with, such as admin or door knocking. This is modelled with a many-to-many relationship between users and interests.
The only extra concept added here is the option, which we used in the previous example. As the name implies, this allows us to destroy child records in addition to creating and editing them. Recall that by default, this behaviour is disabled, so we need to explicitly enable it.
As before, after adding there are two more steps to adding interest check boxes to our form: setting up appropriate default values, and using to create the necessary form fields. Let’s start with the first one:
Once again, when is given the name of a has many relationship, it iterates over every object in that collection and outputs the given fields once for each record. So for a user that has two interests, the above code will create two check boxes, one for each interest.
We know that the allow_destroy option above allows us to send a special attribute that if true will flag the object to be deleted. The problem is that this is the inverse of the default check box behaviour: when the check box is unchecked we want to be true, and when it is checked we want to keep the record around. That is what the last two parameters to do (‘0’ and ‘1’): set the checked and unchecked values respectively, flipping them from their defaults.
While we are in that area, we also need to override the default logic that decides whether the check box is checked initially. This is what does—if the record exists in the database, then the user has indicated they are interested in that area, so the box should be checked. Note here the use of to access the current record in the loop. You can use this method inside any or to get the current object.
I have been talking about checking whether the current record is persisted or not. When you load a user out of the database, of course all the interest records will be persisted. The problem is only those interests already selected will be shown and checked, whereas we actually need to show all interests whether or not they have been selected in the past. This is where we use our method from earlier to provide “default” new records for interests that are not persisted.
First this code creates a new join record for all interests that the user does not currently have selected (), and then uses an in-place sort () to ensure that the check boxes are always shown in a consistent order. Without this, all the new unchecked records would be grouped at the bottom of the list.
You can download the complete code for this article over on github to have a play around with it. Let us know how you go in the comments.
Learn more on ruby with our tutorial Understanding Eigenclasses in Ruby and Realizing Their Importance on SitePoint.
In this article we are going to discuss how to build more complex forms using Rails’ nested attributes feature. I will show you how to manipulate multiple associated records from a single form and properly set up the models and controller to enable this feature. Also, we are going to discuss common pitfalls and power our form up to make it more dynamic using the gem called Cocoon. This solution allows adding and removing nested fields asynchronously while providing lots of customization options and callbacks.
The source code is available at GitHub.
The demo app is available at sitepoint-nested-forms.herokuapp.com.
Building a Simple Form
For this demo I’ll be using Rails 5 but most of the described concepts can be applied to Rails 3 and 4
Go ahead and create a new application without the default testing suite:
Suppose that, with this app, we want to keep track of our favorite places and their addresses. For example, if we enter “Cafe” as a place along with a bunch of addresses of our preferred cafes. This means that one place may have many addresses, so we’ll describe it using associations:
Make sure that the associations are set up properly:
Now code a basic (the one to rule them all…):
Add the routes:
Now, the view for the root page:
Having added , we also need the corresponding partial:
The view to create places:
Including the form:
Here’s yet another partial to display errors:
So far so good. However, in terms of better user experience, I’d like to allow adding place’s addresses on the same page rather than coding a separate form. This will also save us from coding an additional controller to manipulate addresses. That’s where the nested attributes come into play.
Adding Nested Attributes
The idea behind the nested attributes is quite simple. You have a single form where you can create an object along with its associated records. This feature can be added really fast, as it requires very small modifications to the controller and the model, as well as some markup.
It all starts with the addition of the long-named accepts_nested_attributes_for method:
Having added this method, we can now manipulate addresses via the places’ mass-assignment mechanism. The controller requires some changes as well:
When you submit a form with the nested fields, the will contain an array under the key . This array describes each address to be added into the database. As long as we are using strong_params, those new attributes have to be explicitly permitted.
Now add the nested form into the view:
The fields_for method, as you’ve probably guessed, adds the nested fields. It is quite similar to the method but does not provide the tag itself. Note that inside the block I am using a new local variable – do not call it because it already contains the builder for the parent form.
There is a problem, however. When you visit the “New Place” page you won’t see any nested fields, because obviously the new instance of the class does not contain any nested addresses. The simple fix, as suggested by the Rails docs, would be to build a couple of addresses directly in the controller:
Indeed that’s not the best solution and we’ll get rid of it later.
You may now boot the server, navigate to the “New Place” page, and try creating a place with some nested addresses. However, things can’t always go that smooth, right? If you are using Rails 5.0, like me, you’ll see a pretty strange error “Addresses place must exist” preventing the form from being submitted. This appears to be a major bug in Rails 5 that is related to the new option set to . This setting means that the associated record must be present by default. To globally opt-out from this behaviour you may either set to (inside the new_framework_defaults.rb initializer file) or provide the option to the method.
Another fix suggested here involves using the option:
This bug should be fixed in Rails 5.1.
A Bit of Validation
Currently, a user may create a place with a list of empty addresses, which is probably not what you want. To control this behavior, use the option that accepts either a lambda or the value. will reject a record where all the attributes are blank. However, in our case, we want to reject if any attribute is blank, so let’s stick with the lambda:
Now any address without a city or street won’t be saved into the database.
The addresses can now be added, but there is no way to remove them later. To tackle this issue, supply yet another option to the method:
This simply means that now nested records can be destroyed. In order to destroy a nested record, the field has to be set with a truthy value (that is 1, ‘1’, true, or ‘true’). This new field has to be permitted as well:
Add a checkbox to mark the nested records for deletion:
Now code two new controller’s actions:
Note that the actions themselves do not require any special changes, which is really great.
Add two more routes:
And present the “Edit” link:
Now open any existing place, set the checkboxes near the addresses you wish to destroy and submit the form!
Making It Dynamic
Getting started with Cocoon is simple. Add a new gem:
And install it:
Note that Cocoon requires jQuery to be present. Now extract the nested fields into a separate partial:
Here we meet the first Cocoon’s helper – . This helper, as the name implies, create a new link that asynchronously deletes an associated record. This method accepts three arguments (the third one is optional):
- The text to show in the link
- The form object
- HTML options (similar to the ones passed to the )
Note that the class is required for the “remove address” link to work.
Now we need to use this partial inside the form:
Here we are using the second Cocoon’s helper – link_to_add_association. It renders a link to dynamically add nested fields using the partial we’ve coded a minute ago. This method accepts four parameters (the fourth one is optional):
- The text to show in the link
- The form builder (the parent’s form, not the nested’s one!)
- The name of the association
- HTML options. These options are similar to the ones the accepts, however there are some special parameters available (like where to render the nested fields or which partial to use), so be sure to browse the docs
That’s pretty much it! Boot the server and try adding and removing places’ addresses. This is much convenient now, isn’t it?
The last thing I am going to show you today is how to set up Cocoon’s callbacks. There are four of them:
With the you may animate the nested fields’ appearance. Let’s code this in a new CoffeeScript file:
As long as I am using Turbolinks 5, we are listening to the event. If you prefer to stay away from Turbolinks for some reason, the first line will be much simpler:
Require this file:
Inside the callback you may, for example, highlight the added fields. The jQueryUI library has a bunch of effects to pick from – I am going to utilize the “Highlight” effect in this demo.
Add the new gem:
Require a new JS file (note the proper order):
Now utilize this new effect:
To animate an element’s removal, use the callback. There is a small gotcha here, however. The actual removal of the element from the page has to be delayed because otherwise, we won’t be able to animate it.
says Cocoon to delay the element’s removal by 1 second – just enough for us to perform the animation.
Lastly, let’s display how many nested records were added and change that count dynamically. Add a new block:
Next, wriate a simple function that is going to change the counter:
Lastly, update the callback and add a new one called . The final version of the script is presented below:
You may wonder whether it is possible to limit the number of nested records somehow. The accepts_nested_attributes_for method does support the which specifies the maximum number of associated records that can be processed. It can be supplied with an integer, a procedure, or a symbol pointing to a method (both the procedure and the method must return an integer).
Cocoon, however, does not support limiting of the nested records at the time of writing this article. There was an discussion regarding this issue but the author does not consider it to be a core feature. Still, there is an open pull request adding this functionality available that may be merged some time in future.
In this article we’ve discussed the usage of the nested attributes in Rails. We’ve created a basic, nested form allowing users to add, edit, and destroy the associated records. Later we integrated the Cocoon gem and powered our form with jQuery, making it dynamic.
Cocoon has many more options available for customization, so be sure to browse its docs. Hopefully, this article was useful to you. As always, I thank you for staying with me and see you soon!