reference:
Part1: astockwell.com
Part2: astockwell.com
This is a follow-up on Part 1, primarily to address a common headache with creating/updating polymorphic models with a single route. It starts with an error:
Error: Cannot build association medium. Are you trying to build a polymorphic one-to-one?
Briefly, here’s our starting point (see Part 1 for more details on arriving at this configuration):
# painting.rb
class Painting < ActiveRecord::Base
belongs_to :medium, polymorphic: true
accepts_nested_attributes_for :medium
end
# water_painting.rb
class WaterPainting < ActiveRecord::Base
# For your sanity, you may want to use
# :medium instead of :details
has_one :painting, as: :details, dependent: :destroy
end
# oil_painting.rb
class OilPainting < ActiveRecord::Base
# For your sanity, you may want to use
# :medium instead of :details
has_one :painting, as: :details, dependent: :destroy
end
# your_paintings_controller.rb
# ... snip ...
# GET /your_paintings/water/new
def new_water
@painting = Painting.new(medium: WaterPainting.new)
end
# ... snip ...
# GET /your_paintings/oil/new
def new_oil
@painting = Painting.new(medium: OilPainting.new)
end
# ... snip ...
In all likelihood, since you have different types of paintings, they will have different attributes/fields and you’ll want to be able to render different views for each.
The trouble comes when we want to have a generic create method/route for creating any type of painting:
# your_paintings_controller.rb
# POST /your_paintings/create
def create
# This will throw an error!
@painting = Painting.new your_painting_params
respond_to do |format|
if @painting.save
format.html { redirect_to your_painting_path(@painting.id), notice: 'Your painting was successfully created.' }
else
format.html { render :new }
end
end
end
# ... snip ...
def your_painting_params
params.require(:painting).permit!
end
The error is thrown when the POST request comes back to the server (running the generic create method we have above), and there’s no state on the server or in the params that Rails can use to determine which type of painting you intended to create.
I wanted to avoid having n create handlers (n being the total number of painting types) that were basically identical, and realized that I just needed to get the desired type communicated to the POST handler and everything thing could be DRYed out.
I updated each painting type’s new view with the following hidden field:
# new_water.html.erb
<h1>New Water Painting</h1>
<%= simple_form_for @painting, url: { controller: "your_paintings", action: "create" } do |f| %>
<%= f.input :title %>
<%= f.simple_fields_for :medium do |v| %>
<%= v.input :source %>
<% end %>
# The all-important medium_type!
<%= f.hidden_field :medium_type %>
<%= f.button :submit %>
<% end %>
To leverage that field value on the server, I used the following approach to safely create the correct painting type:
# your_paintings_controller.rb
# All the different painting types' models:
PAINTING_TYPES = [WaterPainting, OilPainting]
# ... snip ...
def create
begin
medium_klass = PAINTING_TYPES.detect { |m| your_painting_params[:medium_type].classify.constantize == m }
ensure
unless medium_klass
redirect_to your_paintings_water_path, alert: "Incorrect painting type (klass)" and return
end
end
# Can't use these bad boys yet, Rails thinks
# you're just adding random extra attrs to the
# base Painting model, and doesn't like it much.
# Save them for later though:
medium_params = your_painting_params.delete :medium_attributes
@painting = Painting.new your_painting_params
# Ah! Now bring in the painting type's attrs:
@painting.medium = medium_klass.new medium_params
respond_to do |format|
if @painting.save
format.html { redirect_to your_painting_path(@painting.id), notice: 'Your painting was successfully created.' }
else
format.html { render :new }
end
end
end
# ... snip ...
def your_painting_params
params.require(:painting).permit!
end
This approach creates the Painting base object, then adds the correct association object type and passes the correct attrs to each.