Rails 3 Nested Form for Single Table Inheritance associations

Wed, May 7, 2014 3-minute read

A few weeks ago we had to implement a CRUD form in Rails for a model utilizing Rails’ single table inheritance associations.

We had the following object model:

class Publication < ActiveRecord::Base
  has_many :items
end

class Item < ActiveRecord::Base
  belongs_to :publication

  has_and_belongs_to_many :channels
end

class Image < Item
end

class Video < Item
end

class Post < Item
end

class Channel < ActiveRecord::Base
end

Each Publication consists of a collection of Items, such as Image, Video, etc., whereas each Item is associated with many Channels when it should be published.

We wanted to create a simple form allowing the User to dynamically add a set of Items, and prepare a Publication for publishing.

After looking for a while for a solution to this problem, we ended up using the cocoon gem [0], which provides functionality for dynamic nested forms - the link_to_add_association method, which dynamically renders _items_fields.html.haml and adds the appropriate set of fields to the form, as well as the link_to_remove_association method, which dynamically removes a fieldset from the form.

# _form.html.haml

= simple_form_for @publication, :html => { :multipart => true } do |f|

  = f.simple_fields_for :items do |item|
    = render 'item_fields', :f => item
    = link_to_add_association 'Add a Post', f, :items, :wrap_object => Proc.new { |item| item = Post.new }
    = link_to_add_association 'Add an Image', f, :items, :wrap_object => Proc.new { |item| item = Image.new }
    = link_to_add_association 'Add a Video', f, :items, :wrap_object => Proc.new { |item| item = Video.new }

    = f.button :submit, :disable_with => 'Please wait ...', :class => "btn btn-primary", :value => 'Save'

Considering that our form is for an STI model, the trick here is calling the :wrap_object, a proc especially useful when using the decorator design pattern in Rails (such as STI in Rails), since we want to generate the decorated object - either a Video, Image, or a Post. Then we detect the variety of objects inside _item_fields.html.haml and output the desired fields partial.

# _item_fields.html.haml

- if f.object.type == 'Video'
  = render 'video_fields', :f => f
- elsif f.object.type == 'Image'
  = render 'image_fields', :f => f
- elsif f.object.type == 'Post'
  = render 'post_fields', :f => f

The structure described above allows us to keep our controllers slim and propagate validation for each nested object using Rails internals. If an error occurs it is handled and displayed at the appropriate fields partial.

# publication_controller.rb

def create
  @publication = Publication.new(params[:publication])

  if @publication.save
    respond_to do |format|
      format.html { redirect_to @publication }
    end
  else
    respond_to do |format|
      format.html { render :new }
    end
  end
end

def update
  @publication = Publication.find(params[:id])

  if @publication.update_attributes(params[:publication])
    respond_to do |format|
      format.html { redirect_to @publication }
    end
  else
    respond_to do |format|
      format.html { render :edit }
    end
  end
end

One issue we came across is that once the form passes validation, we want Rails to build the decorated objects, such as Video, Image or Post, instead of the base model Item. In order to fix this we had to override the build_association method inside AssociationReflection

# config/initializers/active_record_association_reflection.rb

class ActiveRecord::Reflection::AssociationReflection
  def build_association(*options)
    if options.first.is_a?(Hash) and options.first[:type].presence
      options.first[:type].to_s.constantize.new(*options)
    else
      klass.new(*options)
    end
  end
end

The end result of all this is a slim CRUD publications_controller providing functionality for the user to create nested Publication objects consisting of different decorated Items.

Thanks to Nathan Van der Auwera for building the cocoon gem!

[0] Cocoon - Dynamic nested forms using jQuery made easy; works with formtastic, simple_form or default forms