Complex Forms ... Just Easier
I love having the ability to easily create/update multiple objects with a single form. There are some nice railscasts by Ryan Bates that describe how to do this in Ruby on Rails. What I needed was a way to make writing the attribute accessor methods easier. I also decided that there just weren’t enough rails plugins out there … so off to kill two birds with one stone.
The plugin is called association-attributes and it can be found on github. The goal was to make the process more semantic and most of all DRY. I decided that since we already had to have a method on the model to handle the association we would pattern after that. So, the two methods available are has_one_attr & has_many_attr.
So for a practical example of what this looks like in your model we will use a persons contact info. We also have a plugin out there that makes contact info easier since that is such a standard need and there is no reason to rewrite that all the time.
class Person < ActiveRecord::Base has_one :address, :as => :addressable, :dependent => :destroy has_one_attr :address has_many :phone_numbers, :as => :phoneable, :dependent => :destroy has_many_attr :phone_numbers end
What that does is creates the methods address_attributes & phone_number_attributes on Person so that we can use them in our form. These newly created methods handle creating & updating existing entries for addresses and phone numbers.
We use HAML for all of our views so these examples are no exception.
- fields_for("person[phone_numbers_attributes][]", @person.phone_numbers.new) do |phf| %label{:for => "number"} Phone Number: = phf.hidden_field :id = phf.text_field :number
A few things to note from this example are:
The fields_for has an extra [] on the first parameter … this is because we have a has_many and that tells the browser to treat that as part of an array so that we can have more than one of that same name and it will get put into the array.
We are only doing a new item but you would use the same technique to display existing items by replacing @person.phone_numbers.new with a variable of the actual phone number.
We have an id hidden field … this is if we use an existing phone number object it will update rather than creating a new object. (it still works without that but deletes the old number and creates a new one)
- fields_for("person[address_attributes]", @person.build_address) do |af| %p %label{:for => "line_1"} Street Address: = af.text_field :line_1 %p %label{:for => "city"} City: = af.text_field :city %p %label{:for => "state"} State: = af.select :state, Address::VALID_STATES.map{|s| [s,s]}.sort %p %label{:for => "zip"} Zip: = af.text_field :zip
In future versions I will have a helper method that writes the fields_for for you as well. Looping through current objects and having an option to put a blank one at the end.
The result of this is that now when you submit a form to create or update a person that has these subforms using fields_for then the appropriate phone number and address objects will get created or updated as well. No more having to create the person first then associate the extra objects later. Even in the case of a has_many where the object is new it will create the object for you then create all the association objects next.