Ever needed to create a form that works with two models? If you have, you know that it's a major pain. It really seems like it should be easier, doesn't it? Powerful, SQL-free associations join our models; helpers build our forms in fewer keystrokes than it takes to call somebody a fanboy in a digg comment. But, as soon as you want to put the pieces together — as soon as you seek multi-model form bliss, the whole system falls apart.
Ryan Bates has popularized a few recipes for cooking up just such a dish (we all owe that man a debt of gratitude, don't we?). Great solutions (thanks Ryan!). There are only two flaws to speak of.
- AJAX-friendliness: What is this - web1? Maybe we should all build our sites in frames, and pepper them with blinking text.
- It's not a plugin: You mean I have to write code!? I thought rails was going to wash my dishes, walk my dog, and build my websites, while I sat outside on the porch smoking cigars, praying to DHH.
Being the compulsive pluginizer that I am, I simply couldn't resist this great opportunity.
attribute_fu
attribute_fu makes multi-model forms so easy you'll be telling all of your friends about it. Unless your friends are serious geeks, though, most of them will probably hang up on you. So, I recommend resisting that urge, unless you're looking for a good bedtime story.
To get started, enable attributes on your association (only has_many for now).
class Project < ActiveRecord::Base
has_many :tasks, :attributes => true
end
Your model will then respond to task_attributes, which accepts a hash; the format of the which is a little bit different from what Ryan describes in his tutorials. Here's an example.
@project.task_attributes => { task_one.id => {:title => "Wash the dishes"},
task_two.id => {:title => "Go grocery shopping"},
:new => {
"0" => {:title => "Write a blog post about AttributeFu"}
}}
Follow the rules, though, and attribute_fu will shield you from the ugly details.
Form Helpers
The plugin comes with a set of conventions (IM IN UR PROJECT, NAMIN UR PARTIALZ). Follow them, and building associated forms is as easy as it should be. Take note of the partial's name, and the conspicuously absent fields_for call.
## _task.html.erb
<p class="task">
<label for="task_title">Title:</label><br/>
<%= f.text_field :title %>
</p>
Rendering one or more of the above forms is just one call away.
## _form.html.erb
<div id="tasks">
<%= f.render_associated_form(@project.tasks) %>
</div>
You may want to add a few blank tasks to the bottom of your form — formerly requiring an extra line in your controller.
<%= f.render_associated_form(@project.tasks, :new => 3) %>
This being web2.0, removing elements from the form with Javascript (but your boss calls it AJAX) is a must.
## _task.html.erb
<p class="task">
<label>Task</label><br/>
<%= f.text_field :title %>
<%= f.remove_link "remove" %>
</p>
Adding elements to the form via Javascript doesn't require any heavy lifting either.
## _form.html.erb
<%= f.add_associated_link('Add Task', @project.tasks.build) %>
It's that easy.
Last but not least (okay, maybe least), if you're one of those people who just has to be different - who absolutely refuses to follow the rules, you can still use attribute_fu (I guess...). See the RDoc for how.
Plugins, Plugins, Get Your Plugins
$ piston import http://svn.jamesgolick.com/attribute_fu/tags/stable vendor/plugins/attribute_fu
Check out the RDoc for more details.
Come join the discussion on the mailing list.


Great! I'm using attribute_fu for a few days and it saves me a lot of work. Thank you very much!
Nice work, James! I just used this on an apartments/new form that needed image uploads on a Photo model... and my views, controller, and model are so clean!
Really great! I've been using Ryan's tutorials for some time, but when you have many child models, the views and models get too much poluted. And I was thinking of write a plugin... you've done it! Congrats!
Have you tested it with "complex" (checkboxes, auto-complete fields, hidden fields etc) child forms?
Lucas - nope, it hasn't had a ton of testing on really complex forms. The way it works isn't terribly complicated, though, so I'd be surprised if there were any major problems.
I'd love to hear how it works for you!
Sure James. I'll give it a try with some complex forms in the next couple days.
Interesting..cant wait to test this out!
For the record though, someone already did make a similar plugin based ono those screencasts:
http://code.google.com/p/multimodel-forms/
I've seen and used multimodel-forms. It suffers from the same AJAX-unfriendliness as Ryan Bates' tutorial; that is why I created AttributeFu.
Also, attribute_fu has some enhancements, like using conventions to minimize keystrokes.
Hope you like it.
It took me 10 minutes to refactor one of my multimodel forms to use AttributeFu. Works great!
Now, time to change other multimodel forms. It's really worth it. I can remove a lot of code, which is always cool.
Thanks!
Thanks for the plugin!
Does the plugin work with hasone (ex.: person hasone address) ? If so, I can replace my little hack (http://hiquepedia.hique.org/rails) by the AttributeFu in my code already.
It seems to me that every call to: f.addassociatedlink('Add Task', @project.tasks.build) creates a new task.
It means that whenever I click 'edit' I see an additional empty task form. It's not a big issue, but I'd prefer not to have this behavior.
What would you suggest to avoid that?
Hey guys - thanks for the comments.
Andrzej - I'm not sure exactly what you mean. Why would edit cause an additional empty task form to be built? This seems like a complicated issue, though; would you mind posting this to the mailing list, or emailing me personally (my address is on the top right)?
Hique - No, it doesn't support has_one, but it wouldn't be hard to add. I don't have a ton of time right now, though; I will try to get to it as soon as I can.
Mr. Golick, This plugin looks intriguing, I will try it out this afternoon. One quick question, does it handle validation in the child object on a form submission? Thanks for your time, Mike
Mike - No, it doesn't handle validation... not by itself. I mean - it won't save if your child models are invalid, and you can handle displaying errors on the front end in the usual way (or using the new form_builder way that hasn't been discussed much). But, attrfu doesn't do anything special.
Thanks.
James - I like what your plugin has to offer, but since it doesn't support validation, how do you suggest I get child model validation to work, as the only way I seem to be able to get validation going just outputs 'Model is Invalid' and not the specific attribute error message.
Hi Adam - it doesn't handle validation on its own, but it can be used to manage the validation of child models. Look in to a method called FormBuilder#errormessageon (there's also something called error_messages).
With that, you should be able to do something like this in your child form:
f.errormessageon(:field)
...which would only display the error for that field. Or -
f.error_messages
...which would display the errors for that instance of that model.
Hope that helps.
I'll have to take a look at this deeper, I've just been using a class inherited from OpenStruct used as a proxy between my ActiveRecord models. This practice isn't that bad since I either import in ActiveRecord::Errors, any other functionalities from ActiveRecord or build my own validation methods... of course I don't mind getting dirty and hackin the place up...
Does this work with has_many :though? I've set it up in my project and the child model fields are not rendering. I'm not doing any of the ajax and there is only one instance of the child model right now.
Nate - I've never tested it with hmt associations, but in theory, it should work.
If you email me or the mailing list with relevant code, I can help you get it going.
Thanks for this wonderful plugin.
I tried to get addassociatedlink to work with an associated_name other than object.class.name.underscore and made this small modification:
Oops i made some kind of textile error. Select the white space to see the code
Will this work with more than 2 child forms. For example, Projects, Tasks, Requirements, or Company, People, Address, etc. I was having a problem getting more than a single level working ith
I haven't tried out the plugin yet, but is there any way to set an upper bound to the number of elements created by addassociatedlink? For example, if I wanted to limit my users to a maximum of 5 tasks...
Have been trying to use attribute_fu with checkboxes, has anyone succeeded in doing this?
with has_many :through, isn't there a dependency in rails for the two models that are being joined already be saved? i tried out hmt and got the expected error message of both records needing an ID before being associated
Hey James, nice plugin, I was always programming the ugly details, I'll check if your plugin saves me the time! Thanks and greets Danny
This is a great plugin! Saves me a lot of time. Has anyone used it together with attachment_fu? It works fine to add multiple attachments to a model but it's harder to display the already added ones when I edit the model. It would be nice to display a different partial if the attachment already has data.
@richard - you could always proxy to two other partials inside of the default one. if new_record? render partial x, else render partial y type of thing.
Is there a way to get this plugin to work with forms that include multiple fields for the child models? For example, let's say that "task" includes two properties, one of which is a radio button.
<%= f.renderassociatedform(@project.tasks, :new => 3) %>
The plugin doesn't seem to include index numbers to discriminate between the 3 forms, so there's no way to map the attributes to separate objects when the form is submitted. Am I missing something? (Also if there's a radio button in the partial, all of the radio button inputs have the same name and are treated as one big radio button group.)
Hey James, I forgot about the feedback I promised a while back.
The plugin worked very well with multiple child and with "complex" forms (text fields, selects, links, checkboxes), no hassle.
The only issue I had was when the user removed all the fields, so the request parameters hash had no reference to the child attributes and nothing was deleted (you mentioned this issue on another post). This was solved by creating an empty hash in the update action, this way:
params[:person][:email_attributes] ||= {}
Thank you!
Thanks for this! I was using (unsuccessfully) the multimodel form plugin, but had needed to tweak it to allow me to specifiy my own partial, it wasn't ajax friendly, and I was having a heck of a time getting it to work on a page that had multiple instances of a model that had it's own submodels...but I switched over to your plugin and was able to get everything to work!
I had to tweak it a little bit, but following your coding conventions, I now have a page that has multiple orders, each with mutliple items associated with it, all updating correctly when the page is posted.
I did run in to the same issue as Lucas regarding the removal of all the associated attributes, and I did the same thing he did to remedy it.
Thanks!
Having a slight issue using your plugin with Rails 2.0.2 or 2.1 Whenever I run a rake task, I get:
rake aborted! stack level too deep /Users/michaellarkin/Development/martin/vendor/plugins/attributefu/lib/attributefu/associations.rb:95:in
has_many_without_association_option' /Users/michaellarkin/Development/martin/vendor/plugins/attribute_fu/lib/attribute_fu/associations.rb:95:inhas_many' /Users/michaellarkin/Development/martin/app/models/style.rb:14 /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:203:inload_without_new_constant_marking' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:203:inload_file' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:342:innew_constants_in' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:202:inload_file' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:94:inrequire_or_load' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:248:inloadmissingconstant' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:453:inconst_missing' /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:465:inconst_missing' /Users/michaellarkin/Development/martin/lib/tasks/martin.rake:143 /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:incall' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:inexecute' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:ineach' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:inexecute' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:508:ininvoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:insynchronize' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:ininvoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:518:ininvoke_prerequisites' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:ineach' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:insend' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:ineach' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:515:ininvoke_prerequisites' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:507:ininvoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:insynchronize' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:ininvoke_with_call_chain' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:494:ininvoke' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1931:ininvoke_task' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:intop_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:ineach' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:intop_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:instandard_exception_handling' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1903:intop_level' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1881:inrun' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:instandardexceptionhandling' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/lib/rake.rb:1878:inrun' /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.1/bin/rake:31 /usr/local/bin/rake:19:inload' /usr/local/bin/rake:19Any ideas?
Forgot to mention that the style model referenced doesn't use the plugin, and it's not referenced by any other models with the plugin either.
@Mike Larkin: For clarification, your tweaks allow one parent-children-grandchildren, ie. Customer-Projects-Tasks, so that I could create 1 customer and on 1 page I could add as many projects with as many tasks each.