Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Nested Model Forms
Posted by michael January 26, 2009 @ 08:55 AM
The most popular request on our new Feedback site was for the ability to easily manage multiple models in a single form. Thankfully Eloy Duran has a patch that does just this. But before we roll it into rails 2.3 we want to get some more feedback from you guys. Eloy's written this brief introduction to the patch, so take a look, and add any feedback you have to the lighthouse ticket.
In Rails it has always been a little tricky to create a single form for both a model and its associations. Last summer, the :accessible option was committed as a first stab at a unified solution for dealing with nested models. However, this feature was pulled before the 2.2 release because it only supported nested models during object creation.
The resulting discussion on the core mailing list and our need for mass assignment have resulted in this patch for ticket #1202 which is now up for scrutinizing.
Since this is a rather large patch we would like to invite you to give it a try. Please report any issues or give feedback about the patch in general. We are especially interested to know how the provided solutions work for applications that allow deletion of nested records through their forms.
Below I'll give a quick tour on what the patch does and how to use it so you have no excuse not to give it a try.
Get the patch
Start by creating a new application:
$ mkdir -p nested-models/vendor
$ cd nested-models/vendor
Vendor your 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 .
An example of nested assignment
Suppose you have a project model with associated tasks:
class Project < ActiveRecord::Base
has_many :tasks
validates_presence_of :name
end
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
end
Now consider the following form which allows you to simultaneously create (or edit) a project and its tasks:
<form>
<div>
<label for="project_name">Project:</label>
<input type="text" name="project_name" />
</div>
<p>
<label for="task_name_1">Task:</label>
<input type="text" name="task_name_1" />
<label for="task_delete_1">Remove:</label>
<input type="checkbox" name="task_delete_1" />
</p>
<p>
<label for="task_name_2">Task:</label>
<input type="text" name="task_name_2" />
<label for="task_delete_2">Remove:</label>
<input type="checkbox" name="task_delete_2" />
</p>
</form>
Before the patch
Before this patch you would have to write a template like this:
<% 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 %>
The controller is pretty much 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
The code above is based on Ryan Bates' complex-form-examples application and from the Advanced Rails Recipes book.
After this patch
First you 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 you could write the following template:
<% 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.
The block is called for each member of the tasks collection. -->
<% project_form.fields_for :tasks do |task_form| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task_form.object.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 much more concise and easier to read.
Granted, the template for this example is only slightly shorter, but it's easy to imagine the difference with more nested models. Or if the Task model had nested models of its own.
Validations
Validations simply work as you'd expect; #valid? will also validate nested models, #save(false) will save without validations, etc.
The only thing to note is that all error messages from the nested models are copied to the parent errors object for error_messages_for. This will probably change in the future, as discussed on the ticket, but that's outside of the scope of this patch.
Let's look at an example where Task validates the presence of its :name attribute:
>> project = Project.first
=> #<Project id: 1, name: "Nested models patches", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15", author_id: 1>
>> project.tasks
=> [#<Task id: 1, project_id: 1, name: "Write 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 2, project_id: 1, name: "Test 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 3, project_id: 1, name: "Create demo app", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 4, project_id: 1, name: "Scrutinize", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">]
>> project.tasks.second.name = ""
=> ""
>> project.valid?
=> false
>> project.errors
=> #<ActiveRecord::Errors:0x23e4b10 @errors={"tasks_name"=>["can't be blank"]}, @base=#<Project id: 1, name: "Nested models patches", …, author_id: 1>>
Transactions
By now you are probably wondering about the consistency of your data when validations passes but saving does not. Consider this Author model which I have rigged to raise an exception after save:
class Author < ActiveRecord::Base
has_many :projects
after_save :raise_exception
def raise_exception
raise 'Oh noes!'
end
end
Here's the Project data before an update:
>> project = Project.first
=> #<Project id: 1, name: "Nested models patches", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15", author_id: 1>
>> project.tasks
=> [#<Task id: 1, project_id: 1, name: "Write 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 2, project_id: 1, name: "Test 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 3, project_id: 1, name: "Create demo app", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 4, project_id: 1, name: "Scrutinize", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">]
>> project.author
=> #<Author id: 1, name: "Eloy", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">
Now let's delete the first Task and change the name of the second Task:
>> project.tasks_attributes = { "1" => { "_delete" => "1" }, "2" => { "name" => "Who needs tests anyway?!" } }
=> {"1"=>{}, "2"=>{"name"=>"Who needs tests anyway?!"}}
>> project.tasks.first.marked_for_destruction?
=> true
>> project.tasks.forty_two
=> nil # Oops, I meant #second of course… ;)
>> project.tasks.second.name
=> "Who needs tests anyway?!"
Finally, let's try to save the Project instance:
>> project.save
RuntimeError: Oh noes!
from /Users/eloy/code/complex-form-examples/app/models/author.rb:9:in `raise_exception_if_needed'
An exception was raised while saving one of the nested models. Now let's see what happened to the data:
SQL (0.1ms) BEGIN
Task Destroy (0.3ms) DELETE FROM `tasks` WHERE `id` = 1
Task Update (0.2ms) UPDATE `tasks` SET `updated_at` = '2009-01-22 11:22:23', `name` = 'Who needs tests anyway?!' WHERE `id` = 2
SQL (17.0ms) ROLLBACK
As you can see, all changes were rolled back. Both updates as well as the removal of records marked for destruction all happen inside the same transaction.
As with any transaction, the attributes of the instance that you were trying to save will not be reset. This means that after this failed transaction the first task is still marked for destruction and the second one still has the new name value.
Conclusion
This patch allows you to create forms for nested model as deep as you would want them to be. Creating, saving, and deleting should all work transparently and inside a single transaction.
Please test this patch on your application, or take a look at my fork of Ryan's complex-form-examples which uses this patch.
Your feedback is much appreciated. However, keep in mind that we can't possibly satisfy everyone's needs all at once. Please suggest additional features or major changes as a separate ticket for after the patch has been applied. The goal is to fix the bugs in this patch first so we all have a clean foundation to build on.
On a final note, I would like to thank David Dollar for the original :accessible implementation, Fingertips (where I work) for sponsoring the time that has gone into this patch, Manfred Stienstra for extensive help on the documentation, and in no particular order Lance Ivy, Michael Koziarski, Ryan Bates, Pratik Naik and Josh Susser for general discussion.
27 comments
Comments
Leave a response
1. Georg Ledermann on 26 Jan 10:42:
Looks very promising! Will this patch be a replacement for the attribute_fu plugin? (http://github.com/giraffesoft/attribute_fu)
2. EppO on 26 Jan 12:06:
So, with this patch, no code is required in the controller ? everything is done in the form and you just need a save call in the controller on the parent model ? amazing !
3. Eloy Duran on 26 Jan 12:30:
@Georg: Reading the README it would seem so yes. But I only skimmed it, so I don’t know about any possible extra features.
@EppO: Correct :)
4. bill on 26 Jan 12:38:
Small thing, but the name should be “accepts_…” rather than “accept_…”
5. Elías on 26 Jan 15:31:
I’ll give a try. Great work guys.
6. josespinal on 26 Jan 19:35:
Great work both in the patch and in this concise article!
Thanks
7. Scott Becker on 26 Jan 23:39:
really appreciate the uptick on communication efforts around here. thanks!
8. sensei on 27 Jan 08:03:
I wonder if it deals with file uploads, too. :) I wonder if it does MULTIPLE layers, and handles “silent” (ie no-data) middle associations, too.
This is a BLOODY BRILLIANT addition to Rails, I have to say, as this is one of the most HUGE faltering steps that beginning Rails users have… Rolling your own version of this is complex and difficult, and yet it’s also something all useful apps have to do.
9. Clément on 27 Jan 08:25:
Looks like a brillant implementation, not requiring javascript, the best from all done in different plugins.
Proper validations with error display where missing from attribute_fu and others. Now they are here. Hooray !
10. que on 27 Jan 10:17:
Great work!!! PS. Divs are not allowed inside paragraphs :)
11. ruby licious on 27 Jan 10:27:
This is great stuff, been looking forward for this to settle down for a long time.
12. Eloy Duran on 27 Jan 10:39:
@Bill: You are right. I have updated the patch to use “accepts_…”.
http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1202-add-attributes-writer-method-for-an-association#ticket-1202-41
Thanks.
13. Antoine on 27 Jan 14:54:
Does the patch work with the belongs_to association instead of the has_many association ? How is possible to create a new task in your example ? Best regards and CONGRAT !!
14. Elad on 27 Jan 14:56:
But what about a dynamic creation of 3 level model relation? (grandparent => create x number of parents => create x number of children).
i don’t see how this patch solves a case like that.
15. Eloy Duran on 27 Jan 18:38:
@Antoine: Yes it does.
@Elad: Works great, see http://gist.github.com/53464 :)
16. Eloy Duran on 27 Jan 19:17:
@Antoine: See the complex-form-examples for a to-one association.
17. antoine on 27 Jan 20:06:
Last question about adding new task.
In my case, the example would be :
playslist has_many songs
when i am adding a song i am using an autosuggestion to display a list of songs which correspond to the keywords I write in the search box.
each answer is a link which I can click on it to add the specific song selected.
On this link, I don’t have access to the form variable in the helper you wrote (task = render(:partial => ‘task’, :locals => { :pf => form, :task => Task.new }) ) because it is after an AJAX call.
How can i add a song with your patch ?
I hope you understand what i mean
Best regards !!
Antoine
18. Eloy Duran on 28 Jan 08:02:
@Antoine: In order to create a new record on a collection association, you only need to provide a key that starts with the string “new_”.
You can see this in the gist I created for Elad, or also in the documentation that comes with the patch.
19. antoine on 28 Jan 11:46:
I undertstand this line :
$(‘tasks’).insert({ bottom: ”#{ escape_javascript task }”.replace(/new_\\d+/g, new_task_id) });
But How do you do AFTER AN AJAX call to write this line, it doesn’t work … form is undefined …
render(:partial => ‘task’, :locals => { :pf => form, :task => Task.new })
20. antoine on 28 Jan 12:47:
maybe it’s a next step … Congrat by all the way !
21. Manfred Stienstra on 28 Jan 13:18:
Antoine, it’s perfectly possible to create the necessary form elements in an AJAX response without a form builder instance. However, it’s probably better to discuss the details of the use of nested model forms somewhere else in the community [1].
[1] http://rubyonrails.org/community
22. James Schorr on 29 Jan 14:11:
Looks great to me!
23. Jonathan Zeis on 02 Feb 04:42:
wow – great!
hmm.. why do i get a hidden input field which resets a selected checkbox?
<input>
24. Glenn Powell on 05 Feb 11:09:
I seem to see a couple problems with this implementation. I’m using the integrated nested functionality in Rails 2.3 (not a patch).
Firstly, when your child (for a 1-to-1) tries to validates_presence_of :parent, you will get an error upon creation because the parent doesn’t have an id yet.
Secondly, I haven’t been able to get any nested forms to go 3 levels deep, since there is a call to:
@object.respond_to?(”#{association_name}_attributes=”)
in the nested_attributes_association? method which will always fail if @object is nil (which it will be for the FormBuilder objects which are creating new records).
Am I missing something crucial here?
25. K3v3n on 11 Feb 02:00:
Could someone update the documentation as mention by bill on 26 Jan 12:38: Small thing, but the name should be “accepts_…” rather than “accept_…”
I’ve been stock by that mistake who seem to affect all other blog on that new Rails feature.
By the way. Great work!
26. mahuangyihao on 13 Feb 07:49:
Now we can excute the removement and modification ,how about the creation while I want to create/edit/delete tasks in the project page.
27. maverick on 17 Feb 10:52:
how do we use this feature with partials.
i have been trying to create the task form using a partial, passing in the form builder of the project form. but it seems that its not working.
<% form_for(@project) do |f| %> <%= render :partial => ‘tasks/task’, :locals => { :task => f } %>
inside the partial
<% fields_for :tasks do |t| %>
is this correct.. or am i doing something wrong?
Ruby on Rails was created by David Heinemeier Hansson, a partner at 37signals,
then extended and improved by a core team of committers and hundreds of open-source contributors.
Rails is released under the MIT license. Ruby under the Ruby License.
"Rails", "Ruby on Rails", and the Rails logo are trademarks of David Heinemeier Hansson. All rights reserved.
Sponsored by
37signals
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment