Cleaner Mailer Views in Rails with the Presenter Pattern
I love the presenter pattern. I use it in all my mailer views.
It’s deceptively simple, but incredibly useful. The idea is: move formatting and conditionals out of the view and into a plain Ruby object. That keeps logic in one place and makes the view easier to read.
I once shared it with a colleague, and they were genuinely blown away. Hopefully, you’ll find it just as useful.
An unrefactored mailer view
This view has some issues that make it a good candidate for refactoring:
- It uses inline date formatting (
strftime
) right in the template. - It calculates
days_left
directly inside the view logic, tying it toDate.today
at render time. - It contains conditional branching that controls not just structure, but wording.
- It handles pluralization manually:
"day#{'s' unless days_left == 1}"
.
Hi <%= @user.name %>,
Just a reminder that your book "<%= @book.title %>" is due on
<%= @book.due_date.strftime("%B %d, %Y") %>.
<% days_left = (@book.due_date - Date.today).to_i %>
<% if days_left <= 2 %>
Please return it as soon as possible.
It's due in just <%= days_left %> day<%= 's' if days_left != 1 %>!
<% else %>
You still have <%= days_left %> day<%= 's' if days_left != 1 %> left.
<% end %>
Thanks,
Your Friendly Library
Same logic, cleaner view
The refactor moved the conditionals, formatting, and pluralization out of the view and into a new presenter class. The result is a clean template with no conditional logic or formatting.
Hi <%= @presenter.user_name %>,
Just a reminder that your book "<%= @presenter.book_title %>" is due on <%= @presenter.due_date %>.
<%= @presenter.return_message %>
Thanks,
Your Friendly Library
How to get there
The new presenter class.
class BookDueSoonPresenter
attr_reader :user, :book
def initialize(user:, book:)
@user = user
@book = book
end
def user_name
user.name
end
def subject
"Reminder: Book Due Soon"
end
def book_title
book.title
end
def due_date
book.due_date.strftime("%B %d, %Y")
end
def days_left
(book.due_date.to_date - Date.today).to_i
end
def return_message
messages.fetch(message_key) % { days: pluralized_days_left }
end
def message_key
days_left <= 2 ? :urgent : :friendly
end
def messages
{
urgent: "Please return it as soon as possible — it's due in just %{days}!",
friendly: "You still have %{days} left."
}
end
def pluralized_days_left
"#{days_left} #{'day'.pluralize(days_left)}"
end
end
Instantiating the presenter in the mailer.
class UserMailer < ApplicationMailer
def book_due_soon(user, book)
@presenter = BookDueSoonPresenter.new(user: user, book: book)
mail(to: user.email, subject: @presenter.subject )
end
end
Testing Before vs After
Before the refactor, testing required parsing rendered email output with string matching or regexes — a fragile way to verify logic.
test "return_message is urgent when due in 1 day" do
user = User.new(name: "Shey")
book = Book.new(title: "Dune", due_date: Date.today + 1)
email = UserMailer.book_due_soon(user, book).body.to_s
assert_includes email, "it's due in just 1 day!"
end
After the Refactor: test the presenter directly instead of the view.
test "return_message is urgent when due in 1 day" do
user = User.new(name: "Shey")
book = Book.new(title: "Dune", due_date: Date.today + 1)
presenter = BookDueSoonPresenter.new(user: user, book: book)
assert_equal "Please return it as soon as possible — it's due in just 1 day!", presenter.return_message
end
It works the same way in controller views as it does in mailers — anywhere you have complex logic in templates.
Hope you found it helpful.