Handmade Rails Presenters
The presenter pattern is one possible way to move logic from your views into a place where it's not only better suited, but can also be reused. Maybe that's the reason it's an essential part of the MVP pattern in other web frameworks. In this article, I want to offer a lightweight implementation that both explains some specialties when used with Rails helpers and doesn't need any external dependencies.
You've probably already heard or even used a gem like Draper, which introduces the presenter pattern to your Rails application. Note that there's some discussion about the difference between decorators and presenters. Strongly considered, the naming Draper adds is a bit off (see this discussion in their Github issues): Draper is a gem for presenters, not decorators. However, one can argue, that a presenter is just a more specialized form of the decorator pattern. And, most important, in the end it doesn't matter. As long as everybody in your team knows where to put things, you could also call presenters something else, like stones.
Thoughtbot has a nice post up on their Robots blog, which gives an excellent overview about the whys and hows of the presenter pattern. I encourage you to check it out, and, if you haven't already done so, subscribe to their blog updates, as they are a very educating read, every time.
The only shortcoming their suggested implementation has is that it's not taking into account integration with Rails' helper methods, like
asset_path. That's where things get interesting. Consider the following, naive implementation of the presenter pattern:
class SettingPresenter < SimpleDelegator def initialize(setting) super(setting) end def status return 'Not status' if super.nil? super end end
What we're using here is the
SimpleDelegator class from Ruby's standard library. SimpleDelegator will just forward all calls to the underlying object (here
setting) when they can't be found within the actual object. Please note that the check for
nil in the
SettingPresenter#status method can and should be exchanged for another pattern: the Null Object pattern. But let's keep that example for now. Let's move on and include a
link method into our
class SettingPresenter < SimpleDelegator include ActionView::Helpers::UrlHelper def initialize(setting) super(setting) end def link link_to 'Settings', settings_path(self) end end
We've included the
ActionView::Helpers::UrlHelper module because that one defines the
link_to method, which we are using to generate the link to your settings page.
Depending on the complexity of your Rails project, the
SettingPresenter#link method might or might not create a correct link to your settings page. Why? Because we don't have a reference to our current view context, the
link_to method will try its best to come up with a valid host, port and other parts of the URL and configuration. But if you're e.g. dynamically changing the subdomain you're linking to from your
ApplicationController based on the username, you will certainly be out of luck, and the link will point into Nirvana. Another set of common problems I've discovered are assets and digests. Everything will work fine in development but will break heavily in your staging, or even worse, production environment.
There's a very easy fix for this: By using the current
view_context you can call
link_to and other view helpers on that view context and they will correctly invoke Rail's helpers. If you had a look into Draper before, you would now understand why you need to utilize the
h object to access Rails helper functions.
class SettingPresenter < SimpleDelegator def initialize(setting, view) super(setting) @view = view end def link @view.link_to 'Settings', @view.settings_path(self) end end # Usage class SettingsController < ApplicationController def show setting = Setting.find_by_id(params[:id]) @setting = SettingPresenter.new(setting, view_context) end end
view_context object already includes all the Rails helpers, there's also no need to include them in the presenter class anymore. One could still argue if it's okay to initialize the presenter in the controller or if the view wouldn't be a better place for this. Our implementation will support both cases - the
view_context is available in both.
And voilà, we have rolled our own implementation of a presenter in 10 lines of Ruby code and now also revealed some of the magic of the Draper gem.
- Jeroen suggested using
__getobj__to access the
statusof the underlying
settingobject within the first example. I've changed that part, as that makes it way cleaner to read. Thanks!