Using dom_id with a presenter (or other objects)
We have been using presenters much more often in our Rails code, with the goal of having views only use “Mustache-level” logic (conditionals, loops, and interpolation). We’ve been happy with the results so far; it really cleans up the views, and moves logic to easily testable objects. In fact, most tests for presenters can be fast specs and still test well.
I’ve gotten the impression that not everyone does presenters the same way, so before getting to the code I’ll clarify. Our presenters are decorator-like, with a restriction that they only transform values, not make any lasting changes to them. Methods are typically “opted-in” for use in the presenter using def_delegators
from Forwardable
. For a simple example, PersonPresenter
might have a method called full_name
that combines first_name
and last_name
; the Person
class only manages the persistance of first_name
and last_name
. That is, presenters wrap an object with methods for presentation to a human.
Not all of Rails makes this pattern easy to use. We ran into an issue recently when calling dom_id()
in a view. This was our code, roughly:
# File: app/presenters/foo_presenter.rb
class FooPresenter
def initialize(foo)
@foo = foo
end
def bar
# ...
end
# ...
end
# File: app/controllers/foos_controller.rb
class FoosController < ApplicationController
def show
foo = Foo.find(params[:id])
# It's nice to only have to expose a single object to the view.
@foo_presenter = FooPresenter.new(foo)
end
# ...
end
# File: app/views/foos/show.html.erb
<span id="<%= dom_id(@foo_presenter) %>"><%= @foo_presenter.bar %></span>
And this is the error we got:
ActionView::Template::Error (undefined method `to_key' for #<FooPresenter:0x007fc782994a98>):
1: <span id="<%= dom_id(foo_presenter) %>"><%= foo_presenter.bar %></span>
[...]
The obvious fix would be to add a to_key
method that just delegates to the model, but that quickly became a rabbit hole. Many more methods were needed, and I quickly gave up.
It turns out that dom_id()
follows a protocol in that it will call to_model
on whatever’s passed in. We can use that to solve our problem. (ActiveRecord
objects just return self
, as you might imagine.) Here’s the fix:
# File: app/presenters/foo_presenter.rb
def FooPresenter
# ... (previous content)
def to_model
@foo
end
end
Now the presenter can be used with dom_id()
, delegating to the presented model. It’s a surprisingly simple solution. :)
(Versions: Ruby 2.0.0, Rails 3.2.12)