Since no one offered an answer to this question, even after generosity, I finally managed to get it to work independently. It shouldn't have been stunning! Hopefully this will be easier to do in Rails 3.0.
Andy's example is a good way to delete records directly, without submitting the form to the server. In this particular case, what I'm really looking for is a way to dynamically add / remove fields before updating the sub-form. This is a slightly different case, because as the fields are deleted, they are not actually deleted until the form is submitted. I will probably end up using both options depending on the situation.
I based my implementation on the Riley dash of complex fork gitub form examples .
First configure 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 of the fields of the PhoneNumber form:
<div class="fields"> <%= f.text_field :description %> <%= f.text_field :number %> </div>
Then write the base 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, which we can duplicate using javascript. We will 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 partial editing:
<% 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 a js templating template. It will send an empty template for each association, but the sentence :reject_if in the model will drop them, leaving only user-created fields. Update: I rethought this project, see below.
This is not true AJAX, since there is no connection on the server that goes beyond loading the page and submitting the form, but I honestly could not find a way to do this after the fact.
In fact, this can provide a better user interface than AJAX, since you do not need to wait for a server response for each additional field until you are done.
Finally, we need to link this 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 dynamic form of barebones! Javascript here is very simple and can be easily done with other frameworks. You can easily replace my application.js code with the prototype + lowpro, for example. The basic idea is that you don't embed giant javascript functions in your markup, and you don't need to write the tedious phone_numbers=() functions in your models. Everything just works. Hurrah!
After some additional testing, I came to the conclusion that the templates must be removed from the <form> fields. Keeping them there means that they are sent back to the server with the rest of the form, and this only creates headaches later.
I added this to the bottom of my layout:
<div id="jstemplates"> <%= yield :jstemplates %> </div
And the new_child_fields_template helper new_child_fields_template :
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 proposals :reject_if from your models and stop worrying about sending templates.