Rails 3 Nested Form for Single Table Inheritance associations
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!