After spending some time refactoring a collection of decorators in a Rails app recently, I found that most of the decorator methods followed one of a small set of patterns.
These examples use code examples based on the Draper gem, but the concepts should apply to any decorator/presenter/exhibition pattern or library.
1. Linking
One scenario that keeps coming up is wanting to link to an object. Rather than
constructing your own using link_to
, the decorated model should know how to
generate a link to itself:
post = PostDecorator.decorate(post)
post.link # => "<a href='...'>...</a>"
This does not seem all that special for regular objects, but consider nested resources or date-based archives requiring complex arguments:
class PostDecorator < ApplicationDecorator
def link
h.link_to title_with_comments, post_path(post, archive_params)
end
def title_with_comments
"#{title} (#{comments_count})"
end
def archive_params
{ :month => created_at.month, :year => created_at.year }
end
end
This logic is best kept out of your view template. It is also better suited for a decorator than a generic helper method. Compare:
post.link
archive_post_link_to(post)
This becomes extra helpful when using delegation:
class AuthorDecorator < ApplicationDecorator
def link
h.link_to full_name, model
end
end
class PostDecorator < ApplicationDecorator
decorates :post
decorates_association :author
delegate :link, :to => :author, :prefix => true
end
post = PostDecorator.decorate(post)
post.author_link # => '<a href="/authors/1">Arjan</a>'
It is a good convention to always have every model be able to generate a sensible link to itself.
2. Filter attribute through helper method
Most custom decorator methods apply a single helper method to the value of an
attribute, like applying number_to_currency
to a product.price
method.
It is easy to write a simple macro for that in the base decorator:
class ApplicationDecorator < Draper::Base
def self.filter_attributes(*args)
options = args.extract_options!
[*args].each do |attr|
define_method attr do
h.send(options.fetch(:with), model.send(attr))
end
end
end
end
This allows you to write a decorator like so:
class ProductDecorator < ApplicationDecorator
filter_attributes :price, :with => :number_to_currency
filter_attributes :created_at,
:published_at,
:with => :relative_time_in_words
end
This pattern allows you to write generic helper modules that can be used anywhere containing the logic you wish to use. The decorator then takes care of consistently applying it.
Note that it is probably best to get rid of your ApplicationHelper
and not
generate any helpers for your controllers either. Its best to group your
helpers in sensibly named modules rather than by controller or in one big junk
drawer.
3. Translate attribute values
Attributes commonly contain values that need to be localized, like a status column (e.g. ‘published’, ‘draft’, etc.). A decorator method would then usually look like this:
class PostDecorator < ApplicationDecorator
def state
h.t('activerecord.attributes.post.states').fetch state.to_sym
end
end
This is pretty tedious. A macro would help clarify intent, so we can create something like the following:
class ApplicationDecorator < Draper::Base
def self.translate_attributes(translations = {})
translations.each do |attribute, key|
define_method attribute do
h.t(key).fetch model.send(attribute).to_s.to_sym
end
end
end
end
This allows us to declare that an attribute value should be translated:
class PostDecorator < ApplicationDecorator
translate_attributes {
:state => 'activerecord.attributes.post.states'
}
end
With the accompanying translation file:
nl:
activerecord:
attributes:
post:
states:
published: Gepubliceerd
draft: Concept
scheduled: Gepland
Mix and match
These macros do not cover all use cases and do not allow easy mixing and matching. When you find yourself wanting to combine such macros on a single attribute, you are probably better off writing an explicit method.