The idea is with a one to many relationship you click a button on the form to add another child. For example, on a personal information form, you could click “new phone” and another phone number subform would be added. A key point is that this does *not* use AJAX and hit the server each time.

This recipe has been around for quite a while. I think it started with Ryan Bates’ Railscasts http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast and I have seen various incarnations around but nothing clean that works on Rails 4.

The models:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :hobbies
end

# app/models/hobby.rb
class Hobby < ActiveRecord::Base
  belongs_to :user
end

The views:

# app/views/user/_form.rb
<%= form_for(@user) do |f| %>
  <%= f.text_field :first_name %><br>
  <%= f.text_field :last_name %><br>
    <!-- This is the plain old Rails nested form which will render all @user.hobbies -->
    <%= f.fields_for(:hobbies) do |hobby| %>
      <%= render partial: 'hobby', locals: {hobby: hobby} %>
    <% end %>

    <!-- this is a place marker where new forms will be inserted by Javascript -->
    <span id='hobbies_insert_location' style='display: none;'>  </span>
    <%= new_hobby_link %>

    <!-- holder for the javascript templates -->
    <% js_templates << new_child_fields_template(f, :hobbies, {form_builder_local: :hobby}) %>

   <%= f.submit class: "btn btn-primary" %> 
<% end %>

<!-- js form templates -->
<%= raw(js_templates.join("\r")) %>


# app/views/user/_hobby.rb
<div class="dynamic-child-form">
  <%= hobby.text_field :hobby_name %>
  <!-- other hobby fields, labels etc -->
  <%= remove_education_link(education) %>  
</div>

The js_templates bit leverages the current form_builder(f) to render us a nifty template in the html page which the javascript can clone and add new hobby forms with. However, we don’t want this template to be actually rendered inside the form because it will muck things up. We want to render it outside the form where it will sit harmlessly in an invisible div. The actual code to generate these is here:

The helpers:
(application_helper or whichever helper you want):

  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, extra_class='')                                   
    link_to(name, "javascript:void(0)",                                                   
            :class => ['add_child', extra_class].join(' '),
            :"data-association" => association)                                           
  end 
    
  def remove_child_link(name, f, extra_class='')
    f.hidden_field(:_destroy) +
      link_to(name, "javascript:void(0)", class: ["remove_child", extra_class].join(' '))
  end

  def js_templates
    @js_templates ||= []
  end

The new_child_fields_template() handles rendering a partial (in this case _hobby.erb) with the appropriate parent object names so the form fields are all named correctly using Rails nested forms naming conventions. In the child form this will look something like

<input type="text" id="user_hobby_attributes_0_name" ...[/language]

The new_child_fields_template() uses new_hobby in place of the 0 because we don't know the index in advance.

The Javascript:

[language="javascript']
$(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();
    var insert_location = $('#' + association + '_insert_location');
      
    insert_location.before(template.replace(regexp, new_id));
    return false;
  });
  
  $('form').on('click', 'a.remove_child', function() {
    var hidden_field = $(this).prev('input[type=hidden]')[0];
    if(hidden_field) {
      hidden_field.value = '1';
    }
    $(this).parents('.dynamic-child-form').hide();
    return false;
  }); 
});

The add_child and remove child methods are bound to the form by the class names in the links we generated. The add_child code does a regex replace on the “new_hobby” string and replaces it with a the current timestamp integer which will be unique so Rails is happy.

The code above was extracted from one of my applications and greatly simplified. If there is anything missing please shoot us a message in the contact form.