Nested models and forms
Sometimes you would like to create one form for a model and some of its associations. This has always been a bit tedious because you had to re-roll a solution everytime. Last year a start on a unified solution was made with the association macro :accessible option. However, because it only supported nesting of models during creation it was pulled out before the 2.2 release.
This lead to quite some discussion on the rubyonrails-core mailing list. Our needs for functionality like this plus the discussions on the mailing list have lead to the patches on #1202 which is now up for scrutinizing.
However, since this is a rather large patch I will give you a quick tour on what it does and how to use it so you can start reporting issues or give feedback in general. We are especially interested in feedback from people that allow deletion of records through their forms, and how the provided solutions work for them.
Getting the patches
Lets start by creating a new application:
$ mkdir -p nested-models/vendor
$ cd nested-models/vendor
And vendor my Rails branch with the nested models patches:
$ git clone git://github.com/alloy/rails.git
$ cd rails
$ git checkout origin/normalized_nested_attributes
$ cd ../..
$ ruby vendor/rails/railties/bin/rails .
From here on the user is expected to know where to put which code.
The goal
Consider a form which would allow the user to simultaneously create (or edit) a project and its tasks:
The models
class Project < ActiveRecord::Base
has_many :tasks
validates_presence_of :name
end
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
end
Before my patch
Previously this meant you would have to write template code like the following:
<% form_for @project do |project_form| %>
<div>
<%= project_form.label :name, 'Project name:' %>
<%= project_form.text_field :name %>
</div>
<% @project.tasks.each do |task| %>
<% new_or_existing = task.new_record? ? 'new' : 'existing' %>
<% prefix = "project[#{new_or_existing}_task_attributes][]" %>
<% fields_for prefix, task do |task_form| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task.new_record? %>
<div>
<%= task_form.label :_delete, 'Remove:' %>
<%= task_form.check_box :_delete %>
</div>
<% end %>
</p>
<% end %>
<% end %>
<%= project_form.submit %>
<% end %>
This snippets is based on Ryan Bates’s complex-form-examples and Advanced Rails Recipes book
The controller is pretty much the same as your average restful controller. The Project model, however, needs to know how to handle the nested attributes:>
class Project < ActiveRecord::Base
after_update :save_tasks
def new_task_attributes=(task_attributes)
task_attributes.each do |attributes|
tasks.build(attributes)
end
end
def existing_task_attributes=(task_attributes)
tasks.reject(&:new_record?).each do |task|
attributes = task_attributes[task.id.to_s]
if attributes['_delete'] == '1'
tasks.delete(task)
else
task.attributes = attributes
end
end
end
private
def save_tasks
tasks.each do |task|
task.save(false)
end
end
validates_associated :tasks
end
After my patch
First tell the Project model to accept nested attributes for its tasks:
class Project < ActiveRecord::Base
has_many :tasks
accept_nested_attributes_for :tasks, :allow_destroy => true
end
Then lets look at the template code:
<% form_for @project do |project_form| %>
<div>
<%= project_form.label :name, 'Project name:' %>
<%= project_form.text_field :name %>
</div>
<!-- Here we call fields_for on the project_form builder instance -->
<% project_form.fields_for :tasks do |task_form, task| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task.new_record? %>
<div>
<%= task_form.label :_delete, 'Remove:' %>
<%= task_form.check_box :_delete %>
</div>
<% end %>
</p>
<% end %>
<% end %>
<%= project_form.submit %>
<% end %>
As you can see this is more readable and concise. Granted, in this example it’s not much compacter, but imagine what the example would have looked like if the project had more nested models. Or if the Task model even had nested models of its own…
With this patch it should be possible to create nested model forms as deep as you would want to. Creating, saving, and deleting should all work transparently and inside one transaction.
Please test the patches on your application, or take a look at my fork of Ryan’s complex-form-examples which uses these patches: https://github.com/alloy/complex-form-examples/