Decorating ActiveRecord
It’s common to use the decorator/presenter pattern to wrap ActiveRecord
objects and add view-specific logic. For example:
Copyclass Profile < SimpleDelegator def title "#{first_name} #{last_name}'s profile" end end # controller class ProfilesController < ApplicationController def show @profile = Profile.new(current_user) end end # some view <h1><%= profile.title %></h1>
Things fall apart
Simple enough. Now let’s look at some examples that leverage
to_partial_path
Archive
. Given this decorator:
Copyclass Profile < SimpleDelegator def to_partial_path "profiles/profile" end end
What happens in this view?
Copy<%= render profile %>
t will not render the profiles/profile
partial but will instead render the users/user
partial. What happened? The Rails rendering code has a conditional that looks like this (simplified):
Copyif object.responds_to?(:to_model) path = object.to_model.to_partial_path else path = object.to_partial_path end
For most objects where you define to_partial_path
, the rendering code will take that second branch and just call to_partial_path
on your object. However, decorated ActiveRecord
objects do respond to to_model
so the rendering will take that first branch. What happens when you call to_model
on a decorator? It delegates to the wrapped user’s implementation which returns itself. This means calling to_model
on the decorator returns the unwrapped user. This all means that User#to_partial_path
gets invoke rather than Profile#to_partial_path
. The solution is to define to_model
on the decorator:
Copyclass Profile < SimpleDelegator def to_model self end # other things end
A matter of form
So all you need to do is define to_model
on your decorator? Not so fast! Rails has some other surprises up its sleeve. How about something like this:
Copy<%= form_for Profile.new(User.new) do |f| %> <% end %>
You might expect this form to POST to /profiles
but instead it will submit to /users
. Isn’t the path based on the class name? Apparently not. Instead the URL is built based on attributes of your object’s model_name
. Since model_name
is delegated to the underlying user, the form submits to /users
. This can be fixed by extending ActiveModel::Naming
(which defines model_name
). Now it’s generating paths for “profiles” instead of “users”.
Playing nicely with ActiveRecord and ActiveModel
If you’re decorating an ActiveRecord
or ActiveModel
object in Rails, you probably want to define the following to ensure the decorator works the way you expect instead of silently delegating to the underlying object:
Copyclass Profile < SimpleDelegator extend ActiveModel::Naming def to_model self end end