I’ve got a project called TellMe, which is a music release notification service. The app does pretty much exactly what I want it do, and now all I need to do is code up some user friendly UIs and deploy it for great victory. I’ve been keeping all of my models pretty thin, which means a lot of related models. This calls for nested forms.
The simplest example of this is the relation between my Release
and ReleaseDate
classes:
The ReleaseDate
class contains information about when a Release
comes out, items that may be relevant for filtering, and is responsible for kicking off the daily release notification process.
I want it to be great. It should do something like this:
When you click ‘Add Release Date’, it should insert a new bullet above it for a new release date with all relevant information. How can we accomplish this?
The first step is to allow the Release
class to accept nested attributes in forms.
allow_destroy
will allow us to destroy the object from the nested form. We want this. There’s another attribute called update_only
which prevents the nested form from creating new objects.
Second, the controller needs to be modified to allow these new parameters in:
If params[:release_date_attributes[:_destroy]]
evaluates to a truthy value, then the record will be marked for deletion. All of these parameters will get updated in a single transaction during @release.save
.
Cool! The back end of the app now works pretty much exactly like I want it to. It gets a params has with a bunch of release date information and updates/creates/destroys accordingly.
Rails is smart enough to do some pretty cool stuff behind-the-scenes with nested form objects thanks to the accepts_nested_attributes_for
method above. The form_builder
object has a method fields_for
which handles much of the hard work. My form code looks something like this at first:
Rails is, as usual, rather intelligent. It knows that Release has-many ReleaseDates and that @release.release_dates
is going to be an array. It will pull all of the objects out of the array and create those fields for each of them. If there aren’t any objects in the collection, then it won’t create anything. That is pretty cool! But it doesn’t let us create new ones – that’s why I’ve added the link_to 'Add Date'
up there. We’re going to come back to it and write up some JavaScript to make it work.
First, I want the ‘Add Date’ link to work dynamically. I’ll add remote: true
to the options. This causes Rails to implement the link as an AJAX request rather than true link. Where will the link go? Since I’m creating a new ReleaseDate
associated with a Release
, I’ll make a route specifically for that: new_release_release_date_path
. The release_dates_controller
will need to set the release appropriately for the view. Lastly, I’ll need an actual new.js.erb
to implement the change.
Here are the changes:
Now, when I click on the link, the console gets a new message. So this is working exactly as I want it to so far!
First, I want to insert a new li
into the form. This will hold the form for the new field. jQuery makes this fairly easy:
This selects the li
with id='add-date'
, goes before it in the list, and inserts a new li
with class='release_date'
and contents hello!
. So now, I just need to append the form to the li
and everything will be set, right? Unfortunately, while the following code looks right, it doesn’t quite work:
Clicking the link creates the new li and it has a form that looks pretty much right, but the data attributes aren’t correct, and the information doesn’t get included in the release_date_attributes
hash in the params.
Check out the resulting HTML:
And when you click the link multiple times, the AJAX forms are going to have the same IDs and names, rendering them worthless. So it won’t be that easy! One solution would be to use the new JavaScript code to scan the previous fields, capture the right elements, and add them. That appears to be precisely the solution that Cocoon uses, and it also includes a bunch of helper methods to make the Rails code rather nice.