# Gemfile
gem 'draper'
# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
delegate_all
def full_name_with_role
"#{object.name} (#{object.role.titleize})"
end
def avatar_image
if object.avatar_url.present?
h.image_tag(object.avatar_url, class: 'avatar')
else
h.image_tag('default-avatar.png', class: 'avatar')
end
end
def formatted_join_date
object.created_at.strftime("%B %d, %Y")
end
def member_since
"Member since #{formatted_join_date}"
end
def status_badge
badge_class = {
'active' => 'badge-success',
'inactive' => 'badge-secondary',
'banned' => 'badge-danger'
}[object.status]
h.content_tag(:span, object.status.titleize, class: "badge #{badge_class}")
end
def posts_count_text
count = object.posts.count
"#{count} #{count == 1 ? 'post' : 'posts'}"
end
def edit_link
return unless h.policy(object).update?
h.link_to 'Edit Profile', h.edit_user_path(object), class: 'btn btn-primary'
end
def social_links
links = []
links << twitter_link if object.twitter_handle.present?
links << github_link if object.github_username.present?
h.safe_join(links, ' ')
end
private
def twitter_link
h.link_to "@#{object.twitter_handle}",
"https://twitter.com/#{object.twitter_handle}",
target: '_blank',
class: 'social-link'
end
def github_link
h.link_to object.github_username,
"https://github.com/#{object.github_username}",
target: '_blank',
class: 'social-link'
end
end
# Controller usage
class UsersController < ApplicationController
def show
user = User.find(params[:id])
@user = user.decorate
# Or: @user = UserDecorator.new(user)
end
def index
users = User.page(params[:page])
@users = users.decorate
# Decorates entire collection
end
end
# View usage
<%= @user.avatar_image %>
<h1><%= @user.full_name_with_role %></h1>
<p><%= @user.member_since %></p>
<%= @user.status_badge %>
<p><%= @user.posts_count_text %></p>
<%= @user.edit_link %>
<div class="social">
<%= @user.social_links %>
</div>
class PostDecorator < Draper::Decorator
delegate_all
decorates_association :user # Auto-decorates associated user
def title_with_status
status_icon = {
'draft' => '📝',
'published' => '✅',
'archived' => '📦'
}[object.status]
"#{status_icon} #{object.title}"
end
def formatted_content
h.simple_format(h.sanitize(object.content))
end
def reading_time
words = object.content.split.size
minutes = (words / 200.0).ceil
"#{minutes} min read"
end
def published_at_text
return "Not published" unless object.published_at
if object.published_at > 1.day.ago
h.time_ago_in_words(object.published_at) + " ago"
else
object.published_at.strftime("%B %d, %Y")
end
end
def author_byline
"by #{user.name} on #{formatted_date}"
end
def share_buttons
h.content_tag :div, class: 'share-buttons' do
[
twitter_share_button,
facebook_share_button,
linkedin_share_button
].join.html_safe
end
end
def tags_list
object.tags.map { |tag|
h.link_to tag.name, h.tag_path(tag), class: 'tag-badge'
}.join(' ').html_safe
end
def comment_summary
count = object.comments.count
return "No comments" if count.zero?
"#{count} #{count == 1 ? 'comment' : 'comments'}"
end
private
def formatted_date
object.published_at&.strftime("%B %d, %Y") || "Draft"
end
def twitter_share_button
url = h.post_url(object)
text = h.truncate(object.title, length: 100)
h.link_to "Tweet",
"https://twitter.com/intent/tweet?url=#{url}&text=#{text}",
target: '_blank',
class: 'btn btn-twitter'
end
def facebook_share_button
h.link_to "Share",
"https://www.facebook.com/sharer/sharer.php?u=#{h.post_url(object)}",
target: '_blank',
class: 'btn btn-facebook'
end
def linkedin_share_button
h.link_to "Share",
"https://www.linkedin.com/sharing/share-offsite/?url=#{h.post_url(object)}",
target: '_blank',
class: 'btn btn-linkedin'
end
end
# Testing decorators
RSpec.describe UserDecorator do
let(:user) { create(:user, name: 'John Doe', role: 'admin') }
let(:decorator) { described_class.new(user) }
describe '#full_name_with_role' do
it 'combines name and titleized role' do
expect(decorator.full_name_with_role).to eq 'John Doe (Admin)'
end
end
describe '#member_since' do
it 'formats join date' do
expect(decorator.member_since).to match /Member since w+ d+, d{4}/
end
end
end
Draper decorators encapsulate view-specific logic, keeping models clean. Decorators wrap models, adding presentation methods without polluting domain logic. I use decorators for formatting, conditional rendering, helper delegation. Decorators access helper methods via h or helpers. They're object-oriented alternative to procedural helpers. Decorating collections applies decorator to each element. Decorators compose—one decorator can delegate to another. Testing decorators is straightforward—no controller/request setup needed. Draper follows Presenter pattern, improving separation of concerns. Understanding when to use decorators vs. helpers vs. view models is key. Decorators shine for rich, contextual presentation logic tied to specific models.