Nested models and forms

Eloy Duran

Sometimes you would like to create one form for a model and some of its associations. This has always been a bit tedious because you had to re-roll a solution everytime. Last year a start on a unified solution was made with the association macro :accessible option. However, because it only supported nesting of models during creation it was pulled out before the 2.2 release.

This lead to quite some discussion on the rubyonrails-core mailing list. Our needs for functionality like this plus the discussions on the mailing list have lead to the patches on #1202 which is now up for scrutinizing.

However, since this is a rather large patch I will give you a quick tour on what it does and how to use it so you can start reporting issues or give feedback in general. We are especially interested in feedback from people that allow deletion of records through their forms, and how the provided solutions work for them.

Getting the patches

Lets start by creating a new application:

$ mkdir -p nested-models/vendor
$ cd nested-models/vendor

And vendor my Rails branch with the nested models patches:

$ git clone git://github.com/alloy/rails.git
$ cd rails
$ git checkout origin/normalized_nested_attributes
$ cd ../..
$ ruby vendor/rails/railties/bin/rails .

From here on the user is expected to know where to put which code.

The goal

Consider a form which would allow the user to simultaneously create (or edit) a project and its tasks:

The models

class Project < ActiveRecord::Base
  has_many :tasks
  
  validates_presence_of :name
end

class Task < ActiveRecord::Base
  belongs_to :project
  
  validates_presence_of :name
end

Before my patch

Previously this meant you would have to write template code like the following:

<% form_for @project do |project_form| %>
  <div>
    <%= project_form.label :name, 'Project name:' %>
    <%= project_form.text_field :name %>
  </div>
  
  <% @project.tasks.each do |task| %>
    <% new_or_existing = task.new_record? ? 'new' : 'existing' %> 
    <% prefix = "project[#{new_or_existing}_task_attributes][]" %> 
    <% fields_for prefix, task do |task_form| %>
      <p>
        <div>
          <%= task_form.label :name, 'Task:' %>
          <%= task_form.text_field :name %>
        </div>
        
        <% unless task.new_record? %>
          <div>
            <%= task_form.label :_delete, 'Remove:' %>
            <%= task_form.check_box :_delete %>
          </div>
        <% end %>
      </p>
    <% end %>
  <% end %>
  
  <%= project_form.submit %>
<% end %>

This snippets is based on Ryan Bates’s complex-form-examples and Advanced Rails Recipes book

The controller is pretty much the same as your average restful controller. The Project model, however, needs to know how to handle the nested attributes:>

class Project < ActiveRecord::Base
  after_update :save_tasks
  
  def new_task_attributes=(task_attributes)
    task_attributes.each do |attributes|
      tasks.build(attributes)
    end 
  end
  
  def existing_task_attributes=(task_attributes)
    tasks.reject(&:new_record?).each do |task|
      attributes = task_attributes[task.id.to_s]
      if attributes['_delete'] == '1'
        tasks.delete(task)
      else
        task.attributes = attributes
      end
    end
  end
  
  private
  
  def save_tasks
    tasks.each do |task|
      task.save(false)
    end
  end
  
  validates_associated :tasks
end

After my patch

First tell the Project model to accept nested attributes for its tasks:

class Project < ActiveRecord::Base
  has_many :tasks
  
  accept_nested_attributes_for :tasks, :allow_destroy => true
end

Then lets look at the template code:

<% form_for @project do |project_form| %>
  <div>
    <%= project_form.label :name, 'Project name:' %>
    <%= project_form.text_field :name %>
  </div>
  
  <!-- Here we call fields_for on the project_form builder instance -->
  <% project_form.fields_for :tasks do |task_form, task| %>
      <p>
        <div>
          <%= task_form.label :name, 'Task:' %>
          <%= task_form.text_field :name %>
        </div>
        
        <% unless task.new_record? %>
          <div>
            <%= task_form.label :_delete, 'Remove:' %>
            <%= task_form.check_box :_delete %>
          </div>
        <% end %>
      </p>
    <% end %>
  <% end %>
  
  <%= project_form.submit %>
<% end %>

As you can see this is more readable and concise. Granted, in this example it’s not much compacter, but imagine what the example would have looked like if the project had more nested models. Or if the Task model even had nested models of its own…

With this patch it should be possible to create nested model forms as deep as you would want to. Creating, saving, and deleting should all work transparently and inside one transaction.

Please test the patches on your application, or take a look at my fork of Ryan’s complex-form-examples which uses these patches: https://github.com/alloy/complex-form-examples/