Nithin Bekal About

Presenters in Rails

25 Sep 2014

When your models are bloated with methods that are only used in views, it might be a good time to refactor them. Moving that logic into helper modules might be OK in some cases, but as the complexity of your views grows, you might want to look at presenters.

Presenters give you an object oriented way to approach view helpers. In this post, I will walk through how we refactored our views to use presenters instead of helper methods or even methods in the model.

Ryan Bates’ excellent Railscasts has an episode on writing presenters from scratch that guided me through this refactor. It’s dives a bit deeper than what I’m covering here, so go check it out after you’ve read this.

Refactoring the views

We have a HAML view where we display the title and publication status of a post. The title is an ActiveRecord field and the publication status is a method in the Post model.

%h1= @post.title
%p= @post.publication_status

The publication_status is shown as the publication date if it is already published. Otherwise we show the string ‘Draft’.

class Post < ActiveRecord::Base
  def publication_status
    published_at || 'Draft'
  end
end

This isn’t too bad. But what if we need to change this to show the publication date in an “X hours ago” format instead? We would love to use Rails’ time_ago_in_words helper method, but it’s not available in the model. The simplest approach here would be to move this into a helper module.

module PostHelper
  def publication_status(post)
    if post.published_at
      time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

This solves our problem, but these view helpers have the disadvantage of putting all helper methods into a single namespace. It would be nice to have a class that contains post related helper methods.

Creating our first presenter

We can create a PostPresenter class that implements the logic that we have in the helper.

class PostPresenter < Struct.new(:post, :view)
  def publication_status
    if post.published_at?
      view.time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

presenter = PostPresenter.new(@post, view_context)
presenter.publications_status  #=> 'Draft'

The view_context here represents an instance of ActionView, which is where we get view helpers like time_ago_in_words. Since PostPresenter does not contain these helper methods, we need to explicitly pass in the view_context from the controller.

Now we run into the problem of needing a #title method since that is also needed in the view. We can avoid this by passing through method calls to the post object if they are not defined in the presenter. Let’s create a BasePresenter class that takes care of this, and inherit all our presenters from this class.

class BasePresenter < SimpleDelegator
  def initialize(model, view)
    @view = view
    super(model)
  end

  def h
    @view
  end
end

By inheriting from Ruby’s builtin SimpleDelegator class and calling super in the initialize method, we make sure that if we call any method that is not defined in the presenter, it passes it on to the post object.

I have also included a handy method #h that simply returns the view context. (This name is used in both Draper and the presenters Railscast, so let’s follow the same convention.) Our presenter, after inheriting from BasePresenter, now looks like this:

class PostPresenter < BasePresenter
  def publication_status
    if @model.published_at?
        h.time_ago_in_words(@model.published_at)
    else
      'Draft'
    end
  end
end

We can instantiate the presenter objects in the controller:

class PostsController < ApplicationController
  def show
    post = Post.find(params[:id])
    @post = PostPresenter.new(post, view_context)
  end
end

Moving presenters out of controllers

To simplify things further, we could avoid instantiating the presenter object in the controller and instead add a helper method that allows us to wrap the ActiveRecord object inside a presenter directly in the view.

module ApplicationHelper
  def present(model)
    klass = "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

This infers the presenter class from the object being passed in and passes the presenter object into a block that we pass in from the view. It allows us to write code that looks like this:

- present(@post) do |post|
  %h2= post.title
  %p= post.author

Custom presenters

The above code assumes that all our presenter classes follow the convention of appending ‘Presenter’ to the model name. I sometimes split presenters into smaller ones that are used in different places. For instance, I could have an AdminPostPresenter which contains helper methods for showing a post in the admin dashboard.

For these cases, I tweaked the present method to accept an optional second argument.

module ApplicationHelper
  def present(model, presenter_class=nil)
    klass = presenter_class || "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

This allows me to call present(@post, AdminPostPresenter) in the view where I use the admin-specific presenter, while continuing to use the present(@post) format elsewhere.

Wrapping up

Using presenters has been a great win for us in maintaining the views. They also have the added benefit of being easier to test. We did try using draper, but given how easy it is to roll our own presenters, we chose the latter option. If maintaining your Rails views is giving you problems, presenters could be a great way to clean things up.

Further reading

Hi, I’m Nithin! This is my blog about programming. Ruby is my programming language of choice and the topic of most of my articles here, but I occasionally also write about Elixir, and sometimes about the books I read. You can use the atom feed if you wish to subscribe to this blog or follow me on Mastodon.