Using the Presenter Pattern in Ruby on Rails

The presenter pattern is a handy design approach that sits between your views and models, helping you keep your code organized by handling view-specific logic in a clean and maintainable way. In a Rails app, it’s a great way to keep your views and controllers slim since the presenter takes care of preparing the data your views need. Sounds interesting, right? Let’s dive in!

What is the presenter pattern

The presenter pattern, sometimes called the “ViewModel” pattern, is essentially an intermediary between the model and the view. While the model handles core business logic and data, the presenter’s job is to prepare the model’s data specifically for display.

Setting Up a Presenter in Rails

Let’s look at how to set up a simple presenter. We’ll follow Rails conventions by creating a new directory for presenters and structuring our code there.

1. Define a new Presenter class:

In the app directory, create a folder named presenters and add a file for your presenter. Let’s say we’re building a presenter for rendering order details.

# app/presenters/order_presenter.rb
class OrderPresenter
  STATUS_BADGE_CLASSES = {
    "pending" => "badge badge-warning",
    "shipped" => "badge badge-success",
    "delivered" => "badge badge-info"
  }.freeze

  DEFAULT_BADGE_CLASS = "badge badge-secondary".freeze

  def initialize(order)
    @order = order
  end

  def status_badge
    STATUS_BADGE_CLASSES.fetch(@order.status, DEFAULT_BADGE_CLASS)
  end

  def total
    format("$%.2f", @order.total_price)
  end

  def item_list
    @order.items.map { |item| "#{item.quantity}x #{item.name}" }.join(", ")
  end

  def delivery_message
    return "Not shipped yet" unless @order.shipped_at

    days_ago = (Date.today - @order.shipped_at.to_date).to_i
    "Shipped #{days_ago} day(s) ago"
  end
end

2. Instantiate the Presenter

After creating the presenter, we need to pass an order record that needs formatting to it. In this example, the presenter has a few methods, including one for displaying the order total in currency format and one for returning the list of order items.

Using the Presenter in Views

With our presenter in place, let’s see how to use it in a view. First we need to create an instance of the OrderPresenter that will be passed to the view in theOrdersController

# app/controllers/orders_controller.rb

class OrdersController < ApplicationController
  def show
    @order =  Order.includes(:items).find(params[:id])
    @order_presenter = OrderPresenter.new(order)
  end
end

Then, in the show.html.erb view:

<!-- app/views/orders/show.html.erb -->
<h1>Order #<%= @order.id %></h1>
<p><strong>Total:</strong> <%= @order_presenter.total %></p>
<p><strong>Status:</strong> <span class="<%= @order_presenter.status_badge %>"><%= @order.status %></span></p>
<p><strong>Items:</strong> <%= @order_presenter.item_list %></p>
<p><strong>Delivery:</strong> <%= @order_presenter.delivery_message %></p>

This approach allows us to keep formatting out of the view, making the view simpler and easier to understand.

Testing Presenters

Testing our presenter logic is straightforward since we’re only testing display-related logic. Here’s what the unit test for the `OrderPresenter` can look like

# spec/presenters/order_presenter_spec.rb
require 'rails_helper'

RSpec.describe OrderPresenter do
  let(:order) { instance_double(Order, status: "shipped", total_price: 149.95, shipped_at: 3.days.ago) }
  let(:items) { [instance_double(Item, quantity: 2, name: "Widget"), instance_double(Item, quantity: 1, name: "Gadget")] }
  let(:presenter) { OrderPresenter.new(order) }

  before do
    allow(order).to receive(:items).and_return(items)
  end

  describe "#status_badge" do
    context "when status is shipped" do
      it "returns success badge" do
        allow(order).to receive(:status).and_return("shipped")
        expect(presenter.status_badge).to eq("badge badge-success")
      end
    end

    context "when status is delivered" do
      it "returns info badge" do
        allow(order).to receive(:status).and_return("delivered")
        expect(presenter.status_badge).to eq("badge badge-info")
      end
    end

    context "when status is pending" do
      it "returns a default badge" do
        allow(order).to receive(:status).and_return("pending")
        expect(presenter.status_badge).to eq("badge badge-default")
      end
    end
  end

  describe "#formatted_total" do
    it "returns the total price as currency" do
      expect(presenter.formatted_total).to eq("$149.95")
    end
  end

  describe "#item_list" do
    it "returns a formatted list of items" do
      expect(presenter.item_list).to eq("2x Widget, 1x Gadget")
    end
  end

  describe "#delivery_message" do
    context "when shipped_at is present" do
      it "returns shipped message with days ago" do
        expect(presenter.delivery_message).to eq("Shipped 3 days ago")
      end
    end

    context "when shipped_at is nil" do
      it "returns not shipped message" do
        allow(order).to receive(:shipped_at).and_return(nil)
        expect(presenter.delivery_message).to eq("Not shipped yet")
      end
    end
  end
end

Do you need a presenter?

Presenters are great for keeping your code clean, but in some cases you might benefit from using a different approach depending on the use case; let’s look at the pros and cons.

1. Using a presenter

Pros:

  • neatly encapsulates view logic: It improves organization by keeping logic out of views and controllers.
  • Encourages OOP principles: Since a presenter is just a Ruby class, you can leverage inheritance, modules, and testing more effectively.
  • Reusable across views: A presenter can be used in multiple views, making it more maintainable
  • Easier to test: unit testing a presenter is straightforward because it behaves like a regular Ruby object.

Cons:

  • Unnecessary abstraction: In cases where there needs to be applied simple formatting for a specific data point in the view, creating a presenter might result in extra added complexity instead of using a simple helper.
  • No built-in Rails support: Unlike helpers, Rails doesn’t provide first-class support for presenters, so you need to manually instantiate them in controllers or views.

2. Using a Helper

Pros:

  • Lightweight and built-in: Rails provides helper methods out of the box, and they are easy to define.
  • Quick and simple for formatting: If all you need is a small formatting method (like formatting a date), a helper is ideal.
  • No need to instantiate anything: Since helpers are included in views automatically, you can call them anywhere.

Cons:

  • Can become messy quickly: Helpers can easily turn into a dumping ground for unrelated methods.

3. Using a View Component

Pros:

  • Encapsulates both logic and UI: Unlike presenters, which only handle logic, ViewComponents also render partials.
  • Better structure for reusable UI: If you have repeated UI patterns, ViewComponents help keep things DRY.
  • Encourages component-driven design: If you’re building a UI-heavy Rails app, ViewComponents make the codebase cleaner.

Cons:

  • Requires additional setup: Unlike helpers and presenters, you need to install and configure the view_component gem.
  • Might feel unnecessary for small Rails apps: In smaller apps without a complex UI, using ViewComponents can add unnecessary complexity.

The presenter pattern is a valuable addition to your Rails toolkit, especially as your application grows in complexity; as highlighted in the above comparison, it’s not the default answer, but when it fits your use case, it can improve the structure and readability of your code.