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.