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.