Since nobody has offered an answer to this, even after a bounty, I've finally managed to get this working myself. This wasn't supposed to be a stumper! Hopefully this will be easier to do in Rails 3.0.
Andy's example is a good way of deleting records directly, without submitting a form to the server. In this particular case, what I'm really looking for is a way to dynamically add/remove fields before doing an update to a nested form. This is a slightly different case, because as the fields are removed, they aren't actually deleted until the form is submitted. I will probably end up using both depending on the situation.
I've based my implementation on Tim Riley's complex-forms-examples fork on github.
First set up the models, and make sure they support nested attributes:
class Person < ActiveRecord::Base
has_many :phone_numbers, :dependent => :destroy
accepts_nested_attributes_for :phone_numbers, :reject_if => lambda { |p| p.values.all?(&:blank?) }, :allow_destroy => true
end
class PhoneNumber < ActiveRecord::Base
belongs_to :person
end
Create a partial view for the PhoneNumber's form fields:
<div class="fields">
<%= f.text_field :description %>
<%= f.text_field :number %>
</div>
Next write a basic edit view for the Person model:
<% form_for @person, :builder => LabeledFormBuilder do |f| -%>
<%= f.text_field :name %>
<%= f.text_field :email %>
<% f.fields_for :phone_numbers do |ph| -%>
<%= render :partial => 'phone_number', :locals => { :f => ph } %>
<% end -%>
<%= f.submit "Save" %>
<% end -%>
This will work by creating a set of template fields for the PhoneNumber model that we can duplicate with javascript. We'll create helper methods in app/helpers/application_helper.rb
for this:
def new_child_fields_template(form_builder, association, options = {})
options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new
options[:partial] ||= association.to_s.singularize
options[:form_builder_local] ||= :f
content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do
form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f|
render(:partial => options[:partial], :locals => { options[:form_builder_local] => f })
end
end
end
def add_child_link(name, association)
link_to(name, "javascript:void(0)", :class => "add_child", :"data-association" => association)
end
def remove_child_link(name, f)
f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child")
end
Now add these helper methods to the edit partial:
<% form_for @person, :builder => LabeledFormBuilder do |f| -%>
<%= f.text_field :name %>
<%= f.text_field :email %>
<% f.fields_for :phone_numbers do |ph| -%>
<%= render :partial => 'phone_number', :locals => { :f => ph } %>
<% end -%>
<p><%= add_child_link "New Phone Number", :phone_numbers %></p>
<%= new_child_fields_template f, :phone_numbers %>
<%= f.submit "Save" %>
<% end -%>
You now have the js templating done. It will submit a blank template for each association, but the :reject_if
clause in the model will discard them, leaving only the user-created fields. Update: I've rethought this design, see below.
This isn't truly AJAX, since there isn't any communication going on to the server beyond the page load and form submit, but I honestly could not find a way to do it after the fact.
In fact this may provide a better user experience than AJAX, since you don't have to wait for a server response for each additional field until you're done.
Finally we need to wire this up with javascript. Add the following to your `public/javascripts/application.js' file:
$(function() {
$('form a.add_child').click(function() {
var association = $(this).attr('data-association');
var template = $('#' + association + '_fields_template').html();
var regexp = new RegExp('new_' + association, 'g');
var new_id = new Date().getTime();
$(this).parent().before(template.replace(regexp, new_id));
return false;
});
$('form a.remove_child').live('click', function() {
var hidden_field = $(this).prev('input[type=hidden]')[0];
if(hidden_field) {
hidden_field.value = '1';
}
$(this).parents('.fields').hide();
return false;
});
});
By this time you should have a barebones dynamic form! The javascript here is really simple, and could easily be done with other frameworks. You could easily replace my application.js
code with prototype + lowpro for instance. The basic idea is that you're not embedding gigantic javascript functions into your markup, and you don't have to write tedious phone_numbers=()
functions in your models. Everything just works. Hooray!
After some further testing, I've concluded that the templates need to be moved out of the <form>
fields. Keeping them there means they get sent back to the server with the rest of the form, and that just creates headaches later.
I've added this to the bottom of my layout:
<div id="jstemplates">
<%= yield :jstemplates %>
</div
And modified the new_child_fields_template
helper:
def new_child_fields_template(form_builder, association, options = {})
options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new
options[:partial] ||= association.to_s.singularize
options[:form_builder_local] ||= :f
content_for :jstemplates do
content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do
form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f|
render(:partial => options[:partial], :locals => { options[:form_builder_local] => f })
end
end
end
end
Now you can remove the :reject_if
clauses from your models and stop worrying about the templates being sent back.