Dynamic Nested Forms in Rails 3

by Andrea Singh | October 07, 2010

Update: The nested_form gem now also works for deeply nested forms. For more details, read Dynamically Nesting Deeply Nested Forms

Some time ago Ryan Bates did a Railscasts episode on dynamic nested forms. He later packaged the code into a plugin that was able to add and remove nested model fields dynamically through JavaScript. Not only is this plugin really light weight, but once installed, its functionality can be incorporated into any form in your app. Some folks have forked the project and started porting it to Rails 3.

After making a few minor tweaks, I converted the plugin to a gem that is fully tested and compatible with Rails 3. If you'd to use this gem in one your Rails 3 apps, you can either clone the gem locally or link to my github repository by adding the following line to your Gemfile:

#Gemfile
gem "nested_form", :git => "git://github.com/madebydna/nested_form.git"

After running bundle:install, issue following command from the root of your app:

$ rails generate nested_form:install

This will generate the nested_form js file which needs to be included after the main jQuery file in your layout:

<%= javascript_include_tag 'jquery', 'nested_form' %>

Whenever you have a nested form that you wish to "ajaxify" (it actually doesn't use ajax at all, since the server never gets called), just use the new form helper called nested_form_for instead of form_for.

<%= nested_form_for @project do |f| %>

Don't worry, this does not override any default behaviour of the form helper. After merging a custom form builder into the options hash, nested_form_for forwards the call to the regular form_for method . What you do get by using nested_form_for, however, are two new methods that enable the dynamic behavior. Those are:

  • link_to_add
  • link_to_remove

And here's how to use them in your nested_form helper:

<%= f.fields_for :tasks do |task_form| %>
  <%= task_form.text_field :name %>
  <%= task_form.link_to_remove "Remove this task" %>
<% end %>
<%= f.link_to_add "Add a task", :tasks %>

Note that link_to_remove needs to be called on the form builder instance of the nested form (here called task_form), while link_to_add gets called on the form builder instance of the parent form (here called f) and is placed outside the nested form.

Even though the gem is meant to just be plugged and played, I found it intriguing to peek under the hood to dissect how it works. A plugin such as this one adds an additional layer of magic and convenience on top of Rails. However, it could not have been built without a solid understanding of how the Rails source code generates its forms.

A Detour Into Source Code For Generating Rails Forms

The form helpers in Rails automagically generate the HTML necessary to ultimately produce a neat params hash, so that the model object can be saved to database.

The very much simplified diagram below attempts to introduce the major players in generating forms with Rails.

form diagram

Whenever you create a new form with the form_for method, the code for which can be found in rails/actionpack/lib/action_view/helpers/form_helper.rb, the method figures out what model object the form is meant for and extracts any options, such as the url the form should be submitted to. However, it only extracts what is needed to create the form tag. The rest of the arguments, including the block which defines the actual form fields are sent on to a fields_for method.

The fields_for method of the FormHelper module

This fields_for method is not the one we commonly see in nested forms view templates. The nested fields_for is an instance method of the FormBuilder class whereas the fields_for method that the parent form_for delegates to is housed in the FormHelper module (see diagram). Here is how the Rails source documentation explains its function:

"[fields_for] creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form."

  
module ActionView::Helpers::FormHelper
# .....

def fields_for(record_or_name_or_array, *args, &block)
  raise ArgumentError, "Missing block" unless block_given?
  options = args.extract_options!

  # figure out object and object_name form record_or_name_or_array

  builder = options[:builder] || ActionView::Base.default_form_builder
  capture(builder.new(object_name, object, self, options, block), &block)
end

# ....
end

From the code above we can see that the parent fields_for method instantiates a FormBuilder object with a bunch of arguments, including self, which is an instance of ActionView::Base, the class that the FormHelper module will be included into. Note that if you have specified a custom form builder in the form_for call (:builder => MyCustomBuilder), it will be used instead of the default form builder.

The last line of the fields_for method "captures" the entire html of the form after evaluating the block that is defined between the do ... end of our form_for invocation.

The first argument passed to capture is the instance of the form builder and the second is the block itself. The capture method yields the FormBuilder instance to the block as the only argument (see diagram). This FormBuilder object - by convention often named f or form - comes equipped with all the form helper methods, such as text_field, label, select that allow for the construction of form tags customized to the model object that the FormBuilder represents.

The fields_for method of the FormBuilder class

One of the FormBuilder's methods is the familiar nested fields_for used to generate nested form fields. The two fields_for methods share their name because they have a similar purpose. Both are responsible for changing scope, that is changing the model object that the form tags are being built for.

Where they differ is that the FormBuilder's fields_for method also needs to determine the relationship between the model object currently in scope and the other one passed in as a parameter.

Once it has done that, it passes its arguments right back to the fields_for method of the FormHelper, which will now yield a new form builder to the block of the nested form. This also allows to switch FormBuilders mid-stream, for instance.

module ActionView::Helpers  
  class FormBuilder  
   def fields_for(record_or_name_or_array, *args, &block)
     # ....
  
     # option to switch form builders for nested form  
     if options[:builder]
       args << {} unless args.last.is_a?(Hash)
       args.last[:builder] ||= options[:builder]
     end

     case record_or_name_or_array
     # ....
     # code to check how record_or_name_or_array is associated with the model 
     # currently in scope
     # constructs the name variable as a string depending on the findings
     # ...
     end
 
     # @template is an instance of the ActionView::Base class
     @template.fields_for(name, *args, &block)
   end
 end
end

How the nested_form Plugin Works

The nested_form basically creates a blueprint for the nested form fields and places it in a hidden division right after the form. A click on the link created by link_to_add will insert an additional nested form through jQuery after the last nested form of its kind.

As we have seen, the nested_form plugin has a custom form builder that sports two additional methods link_to_add and link_to_remove. To employ this alternate form builder, we call upon the services of a new view helper, called nested_form_for.

module NestedForm::ViewHelper

  def nested_form_for(*args, &block)
    options = args.extract_options!.
              reverse_merge(:builder => NestedForm::Builder)
    output = form_for(*(args << options), &block)
    @after_nested_form_callbacks ||= []
    fields = @after_nested_form_callbacks.collect do |callback|
      callback.call
    end
    output << fields.join('').html_safe
  end

  def after_nested_form(&block)
    @after_nested_form_callbacks ||= []
    @after_nested_form_callbacks << block
  end

end

In the code above, we can see that nested_form_for basically merges the :builder option into the form and then passes the arguments on to the regular form_for method. It also iterates over an array of proc objects, calling each each of them in turn. The resulting output is captured and displayed in the template after the form closes.

It is this hidden blueprint of the nested form fields that will get tacked on after the form closes. But how is this accomplished or, to be precise, where does @after_nested_form_callbacks get populated and with what?

To answer these questions we need to look at the code for link_to_add in the custom FormBuilder. In the end, it returns a link that will be picked up by jQuery and will trigger the dynamic behavior of inserting nested forms. Before that happens, though, the method also calls @template.after_nested_form (a method in NestedForm::ViewHelper) which effectively populates the @after_nested_form_callbacks in our custom view helper with the content of the nested form.

module NestedForm
  class Builder < ActionView::Helpers::FormBuilder

    def link_to_add(name, association)
      @fields ||= {}
      @template.after_nested_form do
        model_object = object.class.reflect_on_association(association).klass.new
        output = %Q[<div id="#{association}_fields_blueprint" style="display: none">].html_safe
        output << fields_for(association, model_object, 
                  :child_index => "new_#{association}", 
                  &@fields[association])
        output.safe_concat('</div>')
        output
      end
      @template.link_to(name, "javascript:void(0)", 
                        :class => "add_nested_fields", 
                        "data-association" => association)
    end

  end
end

A Neat Trick

If we look closely, we can see that the most important part of the code, namely the block to be sent into the fields_for method depends on @fields[association], which we've initialized as an empty Hash. The process through which @fields[association] gets a value is based on a simple slight of hand but one that requires familiarity with the ActionView source code.

module NestedForm
  class Builder < ActionView::Helpers::FormBuilder

    def fields_for_with_nested_attributes(association, args, block)
      @fields ||= {}
      @fields[association] = block
      super
    end


    def fields_for_nested_model(name, association, args, block)
      output = '<div class="fields">'.html_safe
      output << super
      output.safe_concat('</div>')
      output
    end

  end
end

The two methods in the code snippet above, fields_for_with_nested_attributes and fields_for_nested_model override the default behavior of FormBuilder objects. They are both being invoked in the course of switching the scope to the child model when fields_for is called. The responsibility of these methods is basically to figure out names and indexes of the child models.

The way they are used here has nothing to do with their core functionality, however. That part gets taken care of by a call to the super class. The crux is that they also afford an opportunity to slurp up the block that was sent to fields_for and save it for later use in the @fields Hash, properly keyed with the name of the child association. So while the form goes about its business creating form fields for the nested model we sneakily snatched a copy of the block and saved it. Note that this happens before link_to_add is called. When it is called eventually it is however equipped with the information it needs to create the hidden blueprint for the nested form. Quite slick indeed.