PolyParent is a plugin designed to help DRY up your controllers and views for polymorphic objects. In other words you want both these URLs to map to the same controller action:
You start out with a
PhoneNumber model that you associate to both your
Location models (through a polymorphic association) with
Location nested below
Customer like so:
map.resources :customers do |customers| customers.resources :locations end
To make both URLs map to the same controller action you modify the above to become:
map.resources :customers do |customers| customers.resources :phone_numbers customers.resources :locations do |locations| locations.resources :phone_numbers end end
Now you have an issue though. When processing requests to
PhoneNumbersController you have no way of knowing where to attach the new
PhoneNumber. How can you avoid havoing two different controller actions doing exactly the same thing? Or even worse, having two different controllers, one dealing with
PhoneNumbers associated to
Customers and the other to
Let's look at how the 'normal' controller actions would look.
The following snippet works when the
PhoneNumbersController was invoked from a
Customer (POST to
class PhoneNumbersController < ApplicationController def new @phone_number = PhoneNumber.new end def create @customer = Customer.find(params[:id]) @customer.phone_numbers.build(params[:phone_number]) if @customer.save flash[:info] = 'A crazy success! A new PhoneNumber is born!' redirect_to customer_phone_numbers_path(@customer) else flash[:error] = @customer.errors.full_messages.to_sentence render :action => :new end end end
When accessed from a
Location context the create action would look like this:
def create @location = Location.find(params[:id]) @location.phone_numbers.build(params[:phone_number]) if @location.save flash[:info] = 'A crazy success! A new PhoneNumber is born!' redirect_to customer_location_phone_numbers_path(@location.customer, @location) else flash[:error] = @location.errors.full_messages.to_sentence render :action => :new end end
The logic is exactly the same but the objects ivolved are different and the paths that need to be generated after the save need different parameters.
By analyzing the request path PolyParent extracts the hierarchy of the nested routes and helps you build generic controller actions that work for all cases.
Step one: prepare the controller
First of all we include the PolyParent module and name the models we wish allow as possible 'parents' of the requests. This allows us to call the controller through other URLs if we need to invoke the it without involving PolyParent.
We call the
set_poly_parents in a before_filter that provides us with a
@parents instance variable (this is not strictly necessary, but mighty convenient for views. See below):
class PhoneNumbersController < ApplicationController include PolyParent parent_resources :location, :customer before_filter :set_poly_parents end
Step two: dry up the actions
Then we proceed by rewriting the controller actions not to make assumptions on the 'parent' resource we're attaching our new PhoneNumber.
def create @parents.last.phone_numbers.build(params[:phone_number]) if @parents.last.save flash[:info] = 'A crazy success! A new PhoneNumber is born!' redirect_to polymorphic_path([@parents, :phone_numbers].flatten) else flash[:error] = @parents.last.errors.full_messages.to_sentence render :action => :new end end
When the create action is invoked for a
PhoneNumber associated with a
@parents will be a one-element Array containing the
Customer instance defined by the
:customer_id key in the params Hash.
When accessed through a
Location, the first element will be the parent
Customer and the last element the parent
Location instance to which we want to attach the
@parents Array is ordered the same way the routes are nested, thus containing the hierarchy information we need to generate the right paths.
Step three: parent agnostic views
Maybe the messiest part of building a reusable controller for our PhoneNumber is the views. The UI will contain the form of course but also a link back to a page listing all the phone numbers (the index view will need links to edit and new as well). All those paths need to be generated polymorphically so they can be used in both the
An example new.html.haml view:
%h2 New Phone Number: - form_for @phone_number, :url => polymorphic_path([@parents, :phone_numbers].flatten) do |f| %fieldset %ul %li.form-field = f.label :number, "Phone Number" = f.text_field :number %li.form-field = submit_tag "Create" = link_to 'Cancel', polymorphic_path([@parents, :phone_numbers].flatten)
To generate a 'new' link, use:
link_to 'new Phone Number', polymorphic_path([:new, @parents, :phone_number].flatten)
To generate an 'edit' link to
link_to 'edit', polymorphic_path([:edit, @parents, @phone_number].flatten)
For some complicated views it is sometimes necessary to know the class of the object being edited/created (for instance for nested forms).
While possible to access the class through
@parents.last.class.class_name, PolyParent provides a
#klassy_name instance method that will return the downcased class name of the parent base class. The base class is the superclass of your model class that inherits from ActiveRecord::Base so it's the same as the class_name for normal models, while for STIed models it returns the base class.
If, for instance, the
Customer model from the examples above inherits from a
Person model, then
klassy_name will return 'person' and the instance in the
@parents Array will be a
Person and not a
Customer, thus allowing you to use the
PhoneNumbersController for all subclasses of