Ruby on Rails
HowToUpdateMultipleAssociatedFormElementsOnOnePage

Source:

http://one.textdrive.com/pipermail/rails/2005-March/004824.html

To restate your problem: an employee has many phone numbers, phone numbers belong to a PhoneNumberType?. You want to edit the employee & their phone numbers & their types in one page?

I think something like this would work:

View:


   <% @employee.phone_numbers.each do |@phone_number| %>
     <%= collection_select "phone_number[]", "phone_number_type_id", 
PhoneNumberType.find_all, :id, :name %>
     <%= text_field "phone_number[]", "number" %>
   <% end %>


Controller:
   @employee.phone_numbers.each_with_index do |phone_number, idx|
     phone_number.update_attributes(@params[:phone_number][idx])
   end

Other Ideas

I was unable to get the above solution to work correctly, but came up with this:

Controller:


@employee.phone_numbers.collect{ |phone_number| 
phone_number.update_attributes( @params[:phone_number][ phone_number.id.to_s ] )
}

@params[:phone_number] is a hash and passing it the record id without first converting it to a string was returning nil.

Alternate Method

If you don’t have the belongs_to object for the PhoneNumber(s) (@employee in the running example) you can use the method below to update the PhoneNumber objects.

Controller:


phone_numbers = PhoneNumber.find( params[:phone_number].keys )

phone_numbers.each do |phone_number|
  phone_number.update_attributes( params[:phone_number][phone_number.id.to_s] )
end


Query
This is all quite lovely, but another problem arises when one wants to support addition and removal of phone_numbers from the employee on the same form.

A straightforward solution would be to use AJAX to call actions for creating & deleting phone_numbers and updating the display appropriately, but this would make the user interface very inconsistent. i.e. some operations would be applied to the server store asyncronously while others were applied only on clicking the commit or edit button of the form page. Seems problematic to me.

The better approach I would say might be to have a bit of java script that can generate form elements for new phone_numbers when a “create” button is clicked, and one that will hide the form elements for a phone_number that has had its delete button clicked (as well as setting say a hidden form element to mark it as deleted).

This really leaves two problems:
1) because the phone_numbers param is a hash, the javascript needs to be able to generate unique id’s for each phone_number block it adds. Not to difficult but needs to be considered in light of..
2) the controller needs an easy method for deciding which elements of the phone_numbers hash are updates, new, or deletes. It would be nice if it could do this without making a database call.

My recommendation I think would be to have the javascript generate id’s off the form phone_number[”-k”] where k is a counter…this would allow the controller to do something like


params[:phone_numbers].each do |id,val|
  if val.has_key? 'delete'
    if id.to_i > 0
      # do destruction
    end
  elsif id.to_i < 0
    # do creation
  else
    # do update
  end
end


Does this make sense to others?

Suggestion
I was having the same problem with creating child rows on the fly, and solved it using a combination of AJAX and Partials, using one partial for both creating and editing. Nothing is created in the database until the submit button is clicked, which is what the user would expect. The trick is to make use of evaluate_remote_response, which lets you return Javascript code from the server to the calling view via AJAX. One disclaimer: I got it working using fields from my own application, but I’ll try to translate to the ongoing “phone number” example.

The partial looks like this (_phone_number.rhtml):


<div>
     <%= collection_select "phone_number[]", "phone_number_type_id",
   PhoneNumberType.find_all, :id, :name %>
     <%= text_field "phone_number[]", "number" %>
</div>

I created a special controller method to be called from the employee view using AJAX:


    def new_phone_number_rows
        @fake_phone_numbers = []                 
1.upto(params[:phone_number_count].to_i) do |idx|
                number = PhoneNumber.new
                number.id = -idx
                @fake_phone_numbers << number
end
        render :layout => false
    end

Here’s the view for that method:


<%
@fake_phone_numbers.each do |@number|
    phone_numbers_html = render(:partial => "phone_number")
-%>
    <%= update_element_function("phone_number_div", :position => :bottom, :content => phone_numbers_html) -%>
<% end -%>

This is the tricky part. I’m making a fake phone_number object to make sure the partial doesn’t break (a Nil exception is thrown if you don’t do this). Also, I’m not rendering actual HTML - only Javascript, which will in turn be called by the employee view. The relevant section of the employee view looks like this:


... in the html head ...
clear_phone_numbers_function = update_element_function("phone_numbers_table", :action => :empty)
update_phone_numbers_function = remote_function(
    :url => { :action => "new_phone_number_rows"},
    :complete => evaluate_remote_response,
    :with => "'phone_number_count='+phone_number_count")

<%= javascript_tag " 
function addPhoneNumberRows(phone_number_count) {
  #{clear_phone_numbers_function}; 
  #{update_phone_numbers_function}
}" 
%>

...

... in the body ...
<select name="phone_number_count" onChange="addPhoneNumberRows(this.value)">
  <option value="1">1</option>
  <option value="2">2</option>
  <option value="3">3</option>
</select>

<div id="phone_number_div"></div>

Here’s what happens: When you change the value of the phone_number_count field, it triggers an AJAX script that calls the action new_phone_number_rows, including the essential parameter phone_number_count (that is passed from the select box). That action then returns Javascript that updates phone_number_div. The ID of each row is negative, as per Aaron Pendergrass’s suggestion. You can use the controller code he presents above. That’s it!

(update – cleaned up code a little—Daniel Tsadok)(update – took object generation out of view – B Gates)