It’s common to use the decorator/presenter pattern to wrap ActiveRecord
objects and add view-specific logic. For example:
class 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
Show archive.org snapshot
. Given this decorator:
class Profile < SimpleDelegator
def to_partial_path
"profiles/profile"
end
end
What happens in this view?
<%= 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):
if 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:
class 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:
<%= 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:
class Profile < SimpleDelegator
extend ActiveModel::Naming
def to_model
self
end
end