Rails presenters for readable, modular and testable code

By Steve Brewer

What is a presenter?

A presenter is a way of isolating all the logic concerned with 'presenting' data to a view.

I learned the approach I use from the developers at FutureLearn, where presenters are common practice, but it's a simpler method than the one proposed by Jay Fields in 2007 that inspired Avdi Grimm's Draper gem.

We don't delegate all methods by default, inherit from anything or pretend the presenter is the model itself. It's just a Ruby class.

But the way Draper describes a presenter here is the same:

If the Model is concerned with storing and manipulating business data, and the View is concerned with displaying it, you can think of the Exhibit [presenter] as standing between them deciding which data to show, and in what order. It may also provide some extra presentation-specific information (such as the specific URLs for related resources) which the business model has no knowledge of by itself.

Technically, exhibit objects are a type of Decorator specialized for presenting models to an end user.

We can use presenters in Rails just by adding them to app/presenters, and in this article I'll explain how I use them and why I like them, using a real problem to solve as an example.

The gist of this approach goes something like this crude example:

Initialise it in the controller: @checkout_presenter = CheckoutPresenter.new(@user, @order)

The view can access anything on @user_presenter and the data returned relates to the @user passed in.

class CheckoutPresenter # Doesn't inherit from anything 
  def initialize(user, order)
    # Can house any objects you initialised it with
    @user = user 
    @order = order
  end

  # Give the presenter read access to @user
  attr_reader :user, :order 

  # Only delegate the methods you need
  delegate :full_name, :orders, to: :user 
  delegate :total, :items, to: :order

  # Define the data to present to the view as methods here
  def is_returning_customer?
    orders.count > 1
  end

  def is_buying_things?
    total.positive?
  end

[More methods]
end

Here's our problem

We need to 'present' dynamic meta data for each page on our website to Twitter and Facebook so the links look nice when they're shared.

But there's a ton of variations between articles on our site.

  • Articles that have a Twitter card attached to them already
  • Articles that don't have a Twitter card
    • Articles that have an HTML title
      • Articles that don't
    • Articles that have a description
      • Articles that don't
    • Articles that have an author who has Twitter
      • Articles that don't

In future we might also have articles with or without images, and other new scenarios to deal with.

This can all get very messy if we put these conditionals in the view or the article model.

It doesn't feel like its own model either - a link sharing meta model? There's no database table, nothing is being 'stored' permanently. We already have our article model that all of this based on, which we don't want to pollute with all these methods either, since they're only concerned with presenting the data. The link sharing meta is data that sits along side articles but isn't really its own 'thing'.

Testing these methods in a model can also be complicated, especially if your different scenarios only really apply to the link sharing situation.

So here's how we might do this with a presenter:

app/presenters/link_meta_presenter.rb

Embedded content: https://gist.github.com/svpersteve/8ef2613ec4e501bd8ba9630a7559fe62.js

This makes it super easy for someone to get the gist of what our app is doing to determine its link sharing data for an article.

It's also easy to change the conditions for deciding what data to return as your situation changes without changing the view, controller or models.

Using the presenter

In the article controller's show action, we initialise a new presenter and pass in the article:

@link_meta_presenter = LinkMetaPresenter.new(@article)

Which we then use in the view to shove into the page's head using the content_for method:

app/views/articles/show.html.haml

- content_for :link_sharing_meta do
  = render 'shared/link_sharing_meta', link_meta_presenter: @link_meta_presenter

app/views/shared/_link_sharing_meta.html.haml

%meta{ property: 'og:title',        content: link_meta_presenter.title }
%meta{ property: 'og:description',  content: link_meta_presenterdescription }
%meta{ property: 'og:url',          content: request.original_url }
%meta{ property: 'og:type',         content: link_meta_presenter.og_type }

%meta{ name: 'twitter:card',        content: link_meta_presenter.card_type }
%meta{ name: 'twitter:site',        content: link_meta_presenter.site }
%meta{ name: 'twitter:title',       content: link_meta_presenter.title }
%meta{ name: 'twitter:description', content: link_meta_presenter.description }
%meta{ name: 'twitter:creator',     content: link_meta_presenter.creator }
%meta{ name: 'twitter:domain',      content: link_meta_presenter.domain }

Lastly, to make it show up, I've included this in the HTML head:

app/views/layouts/application.html.haml

= yield :link_sharing_meta if content_for?(:link_sharing_meta)

How could I test this?

I find presenters much easier to test than models, or views that contain logic. Although it looks long, a lot of it is copied and pasted and altered for each method, which I think is fine when writing specs. A good spec should test effectively and be easy to read, it doesn't need to be militantly short or dry.

spec/presenters/link_meta_presenter_spec.rb

Embedded content: https://gist.github.com/svpersteve/01855413083cfd3f82b757003fa6d121.js

If you make sure you have the following in your .Rspec file:

--color
--format documentation

The output of this test reads like an explanation of the scenarios and what should happen in each:

image

When could I use one?

Presenters are great when you're rendering a show view for an object that could have lots of different states and conditions, but you don't want to add display logic to the model.

A common method I've used in presenters is determining whether something should be shown in a view or not, for example:

Presenter:

delegate :published_at, :price, to: :product

def show_price?
  price.present? && for_sale?
end

def show_buy_button?
  for_sale? && in_stock?
end

def for_sale?
  published_at.present?
end

def in_stock? 
  product.quantity.positive?
end

View:

if presenter.show_price?
  = product.price 
end

if presenter.show_buy_button?
  = link_to 'Buy', buy_path, class: 'button'
end

This makes it very easy to add to the list of conditions required to determine if the price or buy button should be shown or not without having to touch the view, model or controller. And if you use view specs, the spec becomes something like this:

context 'when the price should be shown' do 
  let(:show_price) { true } 

  it 'shows the price' do
    expect(view).to have_content('£100')
  end
end

Why doesn't the presenter inherit from anything?

I don't believe it's necessary to add blanket delegation of all methods to one particular model using SimpleDelegator when creating a presenter. It's over-complicating things in my opinion, and it's less obvious what the presenter is doing.

By delegating only to the methods you actually want, not only can you clearly see from the code which methods exist, but you can delegate to any of the models passed into the presenter when you initialise it.

Presenters as pseudo models

Sometimes people use a presenter to imitate a model, which I think is deceptive. A user presenter isn't a user, why call it one? It's confusing if you see a method called on an article that's pretending to be a presenter, so you check the article model for that method but can't find it.

I've seen people (myself included) do this: @user = UserPresenter.new. People will expect @user to be a User, not a UserPresenter and it will trip them up, especially new developers.

Call a spade a spade, it's a presenter and the presenter is where you should direct people looking for the source of a method.

Why I like presenters

I find it much easier to write specs for presenters than model and view specs, and it keeps the models slim and the views simple.

That extra presenter layer gives you the isolation you need to test views, models and presenters without the complexity that makes testing hard, and it frees you from the constraints imposed by views and models.