rails-presenter
Creates presenter objects for view formatting using SimpleDelegator pattern with TDD. Use when extracting view logic from models, formatting data for display, creating badges/labels, or when user mentions presenters, view models, formatting, or display helpers.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Rails Presenter Generator (TDD)
Creates presenters that wrap models for view-specific formatting with specs first.
Quick Start
- Write failing spec in
spec/presenters/ - Run spec to confirm RED
- Implement presenter extending
BasePresenter - Run spec to confirm GREEN
Project Conventions
Presenters in this project:
- Extend
BasePresenter < SimpleDelegator - Include ActionView helpers for formatting
- Delegate model methods via SimpleDelegator
- Return HTML-safe strings for badges/formatted output
- Use I18n for all user-facing text
BasePresenter (Already Exists)
# app/presenters/base_presenter.rb
class BasePresenter < SimpleDelegator
include ActionView::Helpers::NumberHelper
include ActionView::Helpers::DateHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper
def initialize(model, view_context = nil)
super(model)
@view_context = view_context
end
def model
__getobj__
end
alias_method :object, :model
end
TDD Workflow
Step 1: Create Presenter Spec (RED)
# spec/presenters/[resource]_presenter_spec.rb
RSpec.describe [Resource]Presenter do
let(:resource) { create(:resource, name: "Test", status: :active) }
let(:presenter) { described_class.new(resource) }
describe "delegation" do
it "delegates to the model" do
expect(presenter.name).to eq("Test")
end
it "responds to model methods" do
expect(presenter).to respond_to(:name, :status, :created_at)
end
it "exposes the underlying model" do
expect(presenter.model).to eq(resource)
end
end
describe "#display_name" do
it "returns the formatted name" do
expect(presenter.display_name).to eq("Test")
end
end
describe "#formatted_date" do
context "when date is present" do
before { resource.update(event_date: Date.new(2026, 7, 15)) }
it "returns formatted date in French" do
I18n.with_locale(:fr) do
expect(presenter.formatted_date).to include("2026")
end
end
end
context "when date is nil" do
before { resource.update(event_date: nil) }
it "returns placeholder span" do
result = presenter.formatted_date
expect(result).to include("text-slate-400")
expect(result).to include("italic")
end
end
end
describe "#status_badge" do
it "returns HTML-safe string" do
expect(presenter.status_badge).to be_html_safe
end
it "includes status text" do
expect(presenter.status_badge).to include("Active")
end
it "uses correct color classes for active" do
resource.update(status: :active)
expect(presenter.status_badge).to include("bg-green-100")
end
it "uses correct color classes for inactive" do
resource.update(status: :inactive)
expect(presenter.status_badge).to include("bg-red-100")
end
end
describe "#formatted_currency" do
it "formats cents as euros" do
resource.update(amount_cents: 15000)
expect(presenter.formatted_amount).to eq("150,00 EUR")
end
end
end
Step 2: Run Spec (Confirm RED)
bundle exec rspec spec/presenters/[resource]_presenter_spec.rb
Step 3: Implement Presenter (GREEN)
# app/presenters/[resource]_presenter.rb
class [Resource]Presenter < BasePresenter
# Color mapping for Open/Closed Principle
STATUS_COLORS = {
active: "bg-green-100 text-green-800",
inactive: "bg-red-100 text-red-800",
pending: "bg-yellow-100 text-yellow-800"
}.freeze
DEFAULT_COLOR = "bg-slate-100 text-slate-800"
def display_name
name
end
def formatted_date
return not_specified_span if event_date.nil?
I18n.l(event_date, format: :long)
end
def status_badge
tag.span(
status_text,
class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{status_color}"
)
end
def formatted_amount
return "0,00 EUR" if amount_cents.nil? || amount_cents.zero?
number_to_currency(
amount_cents / 100.0,
unit: "EUR",
separator: ",",
delimiter: " ",
format: "%n %u"
)
end
private
def status_text
I18n.t("activerecord.attributes.[resource].statuses.#{status}", default: status.to_s.humanize)
end
def status_color
STATUS_COLORS.fetch(status.to_sym, DEFAULT_COLOR)
end
def not_specified_span
tag.span(
I18n.t("presenters.common.not_specified"),
class: "text-slate-400 italic"
)
end
end
Step 4: Run Spec (Confirm GREEN)
bundle exec rspec spec/presenters/[resource]_presenter_spec.rb
Common Presenter Methods
Date Formatting
def formatted_event_date
return not_specified_span if event_date.nil?
I18n.l(event_date, format: :long)
end
def short_date
return "—" if event_date.nil?
event_date.strftime("%d/%m/%Y")
end
def days_until
return nil if event_date.nil?
days = (event_date - Date.today).to_i
case days
when 0 then I18n.t("presenters.event.today")
when 1 then I18n.t("presenters.event.tomorrow")
when 2..7 then I18n.t("presenters.event.days_from_now", count: days)
else distance_of_time_in_words_to_now(event_date)
end
end
Currency Formatting
def formatted_budget
return not_specified_span if budget_cents.nil?
number_to_currency(
budget_cents / 100.0,
unit: "EUR",
separator: ",",
delimiter: " ",
format: "%n %u",
precision: 0
)
end
Badge/Tag Generation
def type_badge
tag.span(
display_type,
class: "inline-flex items-center px-2 py-1 rounded text-xs font-medium #{type_color}"
)
end
def display_tags
return not_specified_span if tags.blank?
safe_join(
tags.split(",").map(&:strip).map do |tag_text|
tag.span(tag_text, class: "inline-block bg-slate-100 px-2 py-1 rounded text-xs mr-1")
end
)
end
Contact Links
def display_email
return not_specified_span if email.blank?
mail_to(email, email, class: "text-blue-600 hover:underline")
end
def display_phone
return not_specified_span if phone.blank?
link_to(phone, "tel:#{phone}", class: "text-blue-600 hover:underline")
end
Usage in Controllers
# Single resource
@event = EventPresenter.new(@event)
# Collection
@events = events.map { |e| EventPresenter.new(e) }
# With view context (for route helpers)
@event = EventPresenter.new(@event, view_context)
Checklist
- Spec written first (RED)
- Extends
BasePresenter - Delegation tested
- HTML output is
html_safe - Uses I18n for all text
- Currency stored in cents, displayed in euros
- Color mappings use constants (Open/Closed)
-
not_specified_spanfor nil values - All specs GREEN
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-03-11
- Index updated
- 2026-03-11
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!