Dynamically Nesting Deeply Nested Forms

by Andrea Singh | December 31, 2010

This is an update to my previous post about dynamic nested forms. It was brought to my attention that the functionality of the nested_form gem on github breaks down when used with a form that is nested multiple levels deep.

I remedied this with some tweaks to the code and the gem is now ready to be used on deeply nested forms. I have only tested it for forms up to four levels deep, but there should be no reason why it wouldn't work with even deeper nesting. Of course why anyone would want to nest their forms deeper than that is beyond me. Even four levels can be a real challenge from a usability perspective.

Below is a quick demo on how to set up a deeply nested Rails form for a Project --> Tasks --> Milestones --> Steps:

Nothing special on the Model side of things:

class Project < ActiveRecord::Base
  has_many :tasks, :dependent => :destroy
  validates_presence_of :name
  validates_associated :tasks
  
  accepts_nested_attributes_for :tasks, 
                                :allow_destroy => true, 
                                :reject_if => :all_blank
end

class Task < ActiveRecord::Base
  belongs_to :project
  has_many :milestones, :dependent => :destroy
  
  accepts_nested_attributes_for :milestones, 
                                :allow_destroy => true, 
                                :reject_if => :all_blank
  validates_presence_of :name
  validates_associated :milestones
end

class Milestone < ActiveRecord::Base
  belongs_to :task
  has_many :steps, :dependent => :destroy
  
  accepts_nested_attributes_for :steps, 
                                :allow_destroy => true, 
                                :reject_if => :all_blank
  
  validates_presence_of :name
  validates_associated :steps
end


class Step < ActiveRecord::Base
  belongs_to :milestone
  
  validates_presence_of :name
end

On entering a new Project we would like to be able to dynamically add fields for the project's tasks, as well as the task's milestones and finally for the steps that belong to a milestone. Way easier than it sounds.

The controller code only needs to instantiate a new project instance variable. Note that it is not necessary to pre-build an instance of any of the nested objects.

class ProjectsController < ApplicationController

  def new
    @project = Project.new
  end

end

And here's what the form could look like. I wrote the view code in Haml, not only because I use it all the time, but also because the lack of opening/closing html tags and the indentation might be visually helpful in this case:

= javascript_include_tag 'jquery', 'nested_form'
= nested_form_for @project do |form|
  %fieldset
    %p
      = form.label :name
      %br/
      = form.text_field :name
    = form.fields_for :tasks do |task_form|
      %h4 Task
      = task_form.text_field :name
      = task_form.fields_for :milestones do |milestone_form|
        %h4 Milestone   
        %p
          = milestone_form.text_field :name
        = milestone_form.fields_for :steps do |step_form|
          %h4 Step
          %p
            = step_form.text_field :name
          %p
            = step_form.link_to_remove "[-] Remove this step"
        %p
          = milestone_form.link_to_add "[+] Add A Step", :steps
        %p
          = milestone_form.link_to_remove "[-] Remove this milestone" 
      %p
        = task_form.link_to_add "[+] Add A Milestone", :milestones
      %p      
        = task_form.link_to_remove "[-] Remove this task"
      %hr
    %p
      = form.link_to_add "[+] Add a task", :tasks  

    .actions
      = form.submit 'Save'
      

As you can see, the only complication here is to organize the form in such a way that it makes sense to the user and to keep track of the different Form Builder instances (form, task_form, milestone_form, step_form) in the nested forms. I'm also including a screenshot of what the form actually looks like. I'm aware of the lame design and the confusing number and placement of add and remove links, but I hope that it still manages to illustrate what is possible.

deeply nested form

By the way, the indentation of the nested form visible in the picture is accomplished with a single line of CSS:

.fields {
  margin-left: 15px;
}

This is facilitated by the fact that every newly inserted nested form part will always be wrapped in a div.fields.