This post originated from an RSS feed registered with Ruby Buzz
by rwdaigle.
Original Post: What's New in Edge Rails: Nested Object Forms
Feed Title: Ryan's Scraps
Feed URL: http://feeds.feedburner.com/RyansScraps
Feed Description: Ryan Daigle's various technically inclined rants along w/ the "What's new in Edge Rails" series.
This feature has already been written about on the Rails Blog quite well by Eloy Duran, the committer of this fine feature, so I’ll try not to replicate what’s already out there. However, here’s a basic rundown of what you need to do to get your models nested-form capable.
Step 1: Notify Your Model of Nest-able Associations
The first step is to tell your models which of their associations will be able to receive nested attributes. For all associations you want exposed in nested forms you’ll need to use accepts_nested_attributes_for:
123456789
classPerson < ActiveRecord::Base validates_presence_of :name has_many :children, :class_name => 'Person' accepts_nested_attributes_for :children# can also be used on has_one etc.. associationsend
With this bit in place, you can now directly create, edit and delete children from a person:
12345678910111213141516171819202122232425
# Add a new child to this person@person.children_attributes = [ { :name => 'Son' } ]@person.children #=> [ <#Person: name: 'Son'> ]@person.children.clear# Add two new children to this person@person.children_attributes = [ { :name => 'Son' }, { :name => 'Daughter' ]@person.save@person.children #=> [ <#Person: name: 'Son'>, <#Person: name: 'Daughter'> ]# Edit the son (assuming id == 1)@person.children_attributes = { '1' => { :name => 'Lad' } }@person.save#=> the son's name is now 'Lad'# Edit the daughter (id == 2) and add a new offspring@person.children_attributes = { '2' => { :name => 'Lassie' }, 'new_1' => { :name => 'Pat' } }@person.save#=> the daughter's name is now 'Lassie' and there's a new offspring called 'Pat'# Remove Pat (id = 3), we don't like him/her@person.children_attributes = { '3' => { '_delete' => '1' } }@person.save#=> Pat is now deleted
You’ll want to take away a few things from these examples.
Passing in an array of hashes to the nested attribute setter automatically creates new nested objects.
When mixing the editing of existing nested models and the creating of new ones, we have to use a hash of hashes. The key of the outer hash is the primary key of the item being edited, or new_XX if it represents a nested model to create. The inner hashes are simply the nested model attributes.
To delete an existing nested model, use this format: { 'primary_key' => { '_delete' => '1' } }
While this may appear a bit hackish when you’re used to dealing with the pleasantries of a rich object model and with ActiveRecord’s associations, this provides the foundation for a seamless transition to the view where you need to create your nested model forms…
Step 2: Create a Nested Model Form
In the view, simply use fields_for on these nested models to expose the fields for each such model:
This will create a form with all the form fields necessary for submitting to a RESTful controller, transparently pushing your children_attributes onto the person.
If there are any validation errors on a child, they will be added to person.errors, and nothing will save if any of the children fail (i.e. fully transactional).
A few notes that might be useful to you:
Using fields_for on a has_many association automatically executes once for each nested model present, so think of yourself as being inside a loop when building your child_form
If you ever need to change behavior based on the nested model currently in scope, it can be accessed via child_form.object. In this example we use child_form.object.new_record? to determine whether or not to display the delete checkbox (as that only makes sense on an existing record).
Step 3: In Your Controllers … Do Nothing
The third step should be the easiest, because we’re all dealing with purely RESTful controllers, right? The beauty of this solution is that it takes your controllers out of the mix and makes standard for submissions work perfectly with no interference at the controller level. Just so there’s no confusion, here’s how your create and update actions will look:
Not a peep of those pesky nested models – with the rich support for nested objects at the model layer, it just works!
Extras
As with most powerful features, there are few little tweaks you may find yourself needing.
Default Create Form Fields
Often times you’ll want to have the form displayed with empty fields for easily creating a new nested model. For example, when a user goes to create a new person I want there to be fields for creating a new child already displayed.
Since the person object is brand new they have an empty children collection and no child_form fields will be displayed. There are two ways to get around this:
You can build a new nested object on the controller side (i.e. in the new action):
which will cause there to be empty child_form fields displayed as desired. Or you can do it on the view side with a view helper:
1234567
moduleApplicationHelperdefsetup_person(person) returning(person) do |p| p.children.build if p.children.empty?endendend
Which can then be used within form_for to setup the person to the correct form state:
123
<% form_for setup_person(@person) do |person_form| %> <!-- ... --><% end %>
I prefer this view-helper approach as it really is a view concern (whether or not to display the form fields to create a new nested object by default).
Specify When Nested Models get Built
If you do have empty nested model form fields displayed by default, you’ll run into the issue where the user submits the form with no values filled in and you have to decide if you want to treat that as somebody trying to create a new nested item with no values, or if that means that no new nested item was submitted. Quite often you just want to ignore the submissions with no nested field values filled out.
Although I would have expected this to be default behavior, you need to manually specify that submissions with empty nested values are ignored using the :reject_if option of accepts_nested_attributes_for:
1234567891011
classPerson < ActiveRecord::Base validates_presence_of :name has_many :children, :class_name => 'Person'# This will prevent children_attributes with all empty values to be ignored accepts_nested_attributes_for :children,:reject_if => proc { |attrs| attrs.all? { |k, v| v.blank? } }end
If you want to allow the addition of a large number of nested models via your HTML forms, one option is to just have several empty nested forms displayed by default. This is a little unappealing, however. The far slicker option is to use javascript to dynamically display the new nested form on the user’s request.
Eloy’s example app is also a great place to see how the whole thing works, end-to-end. By far the best resource out there.
Conclusion
So there it is, Rails’ most requested new feature in the flesh and blood. I’m not sure if my experiences are indicative of everybody else’s, but this is a godsend for me. Many thanks to all the folks involved with this functionality (if you don’t know who they are, check out the next section which links to a bunch of great resources). A really great effort by the community.