Ruby on Rails
HowToUpdateMultipleAssociatedFormElementsOnOnePage (Version #12)

Source:

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

So 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 %>
</pre>

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

Other Ideas

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

Controller:

<code>
@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:

<code>
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" %>

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)

Source:

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

So 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 %>
</pre>

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

Other Ideas

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

Controller:

<code>
@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:

<code>
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" %>

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)

Created on August 23, 2006 05:43 by Anonymous Coward (208.18.90.115)