Using Active Decorator
03 Oct 2012
This is a work in progress adapted from http://d.hatena.ne.jp/kitokitoki/20120904/p1
Creating a decorator for the User model. Below is an active_decorator example.
$ rails g decorator user
create app/decorators/user_decorator.rb
invoke test_unit
create test/decorators/user_decorator_test.rb
app/decorators/user_decorator.rb is an empty module.
module UserDecorator
end
Modifying the profile page
In order to edit a profile page with Draper you have to change UsersController#show, but with active_decorator it’s not necessary.
def show
- @user = User.find(params[:id])
+ @user = UserDecorator.find(params[:id]) # you don't need to change this in active_decorator
end
In that case we can start to arrange the view, first by changing the code that displays the user’s avatar.
That portion of the view can be replaced with the following code:
module UserDecorator
def avatar
link_to_if url.present?, image_tag("avatars/#{avatar_name}", class: "avatar"), url
end
private
def avatar_name
if avatar_image_name.present?
avatar_image_name
else
"default.png"
end
end
end
Next, we’ll change the code to display the username. We’ll replace this code.
<h1><%= link_to_if @user.url.present?,
(@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>
We’ll rewrite it this way:
<h1><%= @user.linked_name %></h1>
The decorator is changed as below. It’s roughly the same as the avatar_name method.
def linked_name
site_link(full_name.present? ? full_name : username)
end
private
def site_link(content)
link_to_if url.present?, content, url
end
The template has gotten nicer in ten minutes, but there’s still room for improvement. Next we’ll refactor a large part within the view code. This code snippet shows a link to the user’s web site.
<dt>Website:</dt>
<dd>
<% if @user.url.present? %>
<%= link_to @user.url, @user.url %>
<% else %>
<span class="none">None given</span>
<% end %>
</dd>
We’ll replace it with the contents below.
<dt>Website:</dt>
<dd>
<% if @user.url.present? %>
<%= link_to @user.url, @user.url %>
<% else %>
<span class="none">None given</span>
<% end %>
</dd>
We’ll make a similar method in the decorator.
def website
if url.present?
link_to url, url
else
content_tag :span, "None given", class: "none"
end
end
We can use the same technique in the template section when showing a user’s Twitter info and a user’s bio.
<dt>Twitter:</dt>
<dd>
<% if @user.twitter_name.present? %>
<%= link_to @user.twitter_name, "http://twitter.com/#{@user.twitter_name}" %>
<% else %>
<span class="none">None given</span>
<% end %>
</dd>
<dt>Bio:</dt>
<dd>
<% if @user.bio.present? %>
<%=raw Redcarpet.new(@user.bio, :hard_wrap, :filter_html, :autolink).to_html %>
<% else %>
<span class="none">None given</span>
<% end %>
</dd>
like this:
<dt>Twitter:</dt>
<dd><%= @user.twitter %></dd>
<dt>Bio:</dt>
<dd><%= @user.bio %>
</dd>
# View change
- <dd><%= @user.bio %></dd>
+ <dd><%= @user.deco_bio %></dd>
We will write the decorator like so:
def twitter
if twitter_name.present?
link_to twitter_name, "http://twitter.com/#{twitter_name}"
else
content_tag :span, "None given", class: "none"
end
end
def deco_bio # we can't use the method 'bio' so we're avoiding duplication
if bio.present?
Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
else
content_tag :span, "None given", class: "none"
end
end
We’ll pull out the else clause repeated in the website()
, twitter()
, bio()
/deco_bio()
methods.
def website
handle_none url do
link_to url, url
end
end
def twitter
handle_none twitter_name do
link_to twitter_name, "http://twitter.com/#{twitter_name}"
end
end
def deco_bio
handle_none bio do
Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
end
end
private
def handle_none(value)
if value.present?
yield
else
content_tag :span, "None given", class: "none"
end
end
Another correction we can make — pulling Markdown’s display processing into ApplicationDecorator and calling that method from another decorator. We’ll create a new markdown method that handles an expression with text passed to it.
▼ active_decorator / application_decorator.rb
module ApplicationDecorator
def markdown(text)
Redcarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe
end
end
▼ active_decorator / user_decorator.rb
module UserDecorator
include ApplicationDecorator
...
# def deco_bio
# handle_none bio do
# Redcarpet.new(bio, :hard_wrap, :filter_html, :autolink).to_html.html_safe
# end
# end
def deco_bio
handle_none bio do
markdown bio
end
end
Modifying the Model
Now that we’ve got the decorator set up the way we want, we’ll have a look at the model and if there’s any view-related code we’ll deal with it by moving it to the decorator. For example, in the User model, we are formatting the time that the user is created with the member_since method. This code can be considered view related since it only returns a formatted string. This is moved to the decorator.
▼ Model: /app/models/user.rb
class User < ActiveRecord::Base
# def member_since
# created_at.strftime("%B %e, %Y")
# end
end
▼ Decorator: /app/decorators/user_decorator.rb
def member_since
created_at.strftime("%B %e, %Y")
end
In active_decorator there is no way to restrict access to model methods as there is in Draper, another decorator gem.
Hopefully this helps you to get your feet wet with the active_decorator gem. I (Cameron) started using this at work and am enjoying it. I’m not a native Japanese speaker, but the examples here should be straightforward enough for basic use cases.