I recently had to put together a particularly ugly web form, with dynamically-expanding multi-nested elements, and came up against some rather odd behavior from Rails’ nested forms, using Rails 2.3.8. First off, I have to give thanks to Railscasts for saving me a bunch of time creating the dynamic portion of the nested form — see that episode and the following one for a great solution which got me started. Secondly, note that in Rails 2.3.5, some nested forms behavior was simply broken, and when I upgraded to 2.3.8 it fixed a number of small issues.
Unfortunately, for the form I was working on I had to nest two levels deep, which complicated things. The relationship was something like this: the form was created to update an event, which can involve multiple companies. For each involved company, there is additional metadata. So there are three models involved: Event, CompanyEvent, and Company. CompanyEvent is more than just a join model, since it contains metadata about the relationship. In theory, the nested-nested models weren’t a problem, simply by putting the proper directive in each model:
class Event < ActiveRecord::Base has_many :company_events accepts_nested_attributes_for :company_events, :allow_destroy => true end class CompanyEvent < ActiveRecord::Base belongs_to :company accepts_nested_attributes_for :company belongs_to :event end class Company < ActiveRecord::Base has_many :company_events has_many :events, :through => :company_events end
Thanks to the
accepts_nested_attributes_for directives, the form for an event can easily incorporate entries for company events, which in turn incorporate companies. As an example:
<% form_for(@event, :url => event_path(@event)) do |f| %> <%= f.text_field :event_title %> <% f.fields_for :company_events do |builder| %> <%= builder.text_field :company_role %> <% builder.fields_for(:company) do |company_form| -%> <%= company_form.text_field :name %> <% end -%> <% end -%> <% end -%>
The above is a slimmed-down form, of course, but serves to demonstrate the nested form approach. The event form is the outer one, which contains
fields_for the company_events model — each current instance of a company_event associated with the event will be rendered with its company_role (for example, what role the company plays at the event in question). Within that nested form, another
fields_for is included for the company, which will pull in the company name as a field.
As shown in the Railscast linked above, you can also include a field to mark a nested entry as deleted, such as
. Check out the Railscast for a full demonstration, since there’s no need to repeat it here.
The issue I encountered, though, was mysterious: when I tried to change the name of a company in the nested form, I got an error: “Couldn’t find Company with ID=12345 for CompanyEvent with ID=6789″. This didn’t make much sense, since obviously there wouldn’t be a matching entry, because I was changing the company and thus the company id would have also changed! It was a mystery why the code would be trying to find a matching row using both ids. I actually had to go into the code for nested_attributes.rb and look into the
assign_nested_attributes_for_one_to_one_association method to see what was going on. The key to it seemed to be the use of the
:update_only option on the
accepts_nested_attributes_for directive. I added that to the CompanyEvent model:
class CompanyEvent < ActiveRecord::Base belongs_to :company accepts_nested_attributes_for :company, :update_only => true belongs_to :event end
And suddenly it worked. The slim documentation for the
:update_only option wasn’t very helpful (see http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), as it says that “an existing record may only be updated” and “A new record may only be created when there is no existing record.” Which seems rather obvious, but almost implies that a record can’t be deleted, since it’s “update only”. Otherwise, why on earth would you not want an existing record to be updated? Perhaps this should be the default behavior, though I haven’t tested to figure out what the alternative really means. I need to look at Rails 3 and see what’s changed about the nested forms behavior, and perhaps this is mapped out more clearly there.
In any case, perhaps this will save someone else some pain, since it took me some time to work out what was going on. And I realize that I’ve skimmed over a lot of details of how to do nested forms, since this isn’t intended to be a how-to but more of a watch-out post. If anyone thinks that a general nested-forms how-to post would be useful, let me know and I can put one together.