homeASCIIcasts

286: Draper 

(view original Railscast)

Other translations: Ja Ru

In this episode we’ll take a look a Draper, a gem that lets us add decorators to a Rails application’s views much like a presenter pattern. If you find that you have a lot of complex view logic in your templates and helper methods Draper can help to clean up this code by taking a more object-orientated approach. We’ll show you how it works in this episode.

The application we’ll be working with is shown below. It has a user profile page that shows various pieces of information about a given user including their avatar, full name, username, a short biography in Markdown and links to a website and Twitter feed. If a user has supplied a website the avatar and full name will link to that site.

The profile page for a user who has entered all of their details.

The page seems fairly simple but we also have to handle those users who haven’t entered so much data such as “MrMystery”.

The profile page for a user who has entered few details.

This user has only entered a username so we display that in place of their full name, show a default avatar and some placeholder text for the other fields. This makes the template for this page more complex, with many if conditions needed to handle users with different amounts of information. We could make this template much cleaner if we could move some of this logic out to somewhere else.

/app/views/users/show.html.erb

<div id="profile">
  <%= link_to_if @user.url.present?, ↵ 
  image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), ↵
  @user.url %>
  <h1><%= link_to_if @user.url.present?, ↵
    (@user.full_name.present? ? @user.full_name : ↵
    @user.username), @user.url %></h1>
  <dl>
    <dt>Username:</dt>
    <dd><%= @user.username %></dd>
    <dt>Member Since:</dt>
    <dd><%= @user.member_since %></dd>
    <dt>Website:</dt>
    <dd>
    <% if @user.url.present? %>
      <%= link_to @user.url, @user.url %>
    <% else %>
      <span class="none">None given</span>
    <% end %>
    </dd>
    <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>
  </dl>
</div>

As this logic is view-related we can’t really extract it out into the model. One solution for this would be to use helper methods. We already use one called image_tag in this template to render the avatar. Let’s take a look at it.

/app/helpers/users_helper.rb

module UsersHelper
  def avatar_name(user)
    if user.avatar_image_name.present?
      user.avatar_image_name
    else
      "default.png"
    end
  end
end

This helper method determines whether the current user has an avatar and returns the name of a default image if they don’t. We could extract more of the logic from the view into helper methods but the problem with is that they’re simple methods in a global namespace; there’s nothing object-orientated about them.

Installing Draper

This scenario is a good case for using a presenter, or a decorator as Draper refers to them, so let’s add it to our application. The Draper gem is installed in the usual way, by adding it to the Gemfile then running bundle.

/Gemfile

source 'http://rubygems.org'
gem 'rails', '3.1.0'
gem 'sqlite3'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails', "  ~> 3.1.0"
  gem 'coffee-rails', "~> 3.1.0"
  gem 'uglifier'
end
gem 'jquery-rails'
gem 'redcarpet'
gem 'draper'

Once Draper has installed we’ll create a decorator for our User model by running the draper:decorator generator.

$ rails g draper:decorator user
      create  app/decorators
      create  app/decorators/application_decorator.rb
      create  app/decorators/user_decorator.rb

As this is our first decorator an application_decorator will also be generated. Any decorators we generate inherit from ApplicationDecorator so we can place any functionality that we want to share across decorators there.

The UserDecorator class is fairly straightforward, consisting mainly of comments that explain how it works. We’ll dive right in and start using it to clean up our templates.

Tidying Up The Profile Page

To use Draper in our profile page we first need to make a change to the show action in the UsersController. This action currently fetches a User in the usual way.

/app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end
  def show
    @user = User.find(params[:id])
  end
end

We need to wrap this user in our decorator which we do by replacing User.find with UserDecorator.find.

/app/controllers/users_controller.rb

def show
  @user = UserDecorator.find(params[:id])
end

This code will now return a UserDecorator instance that wraps the User record and delegates all methods to it by default (more on this later). The action will still work as it did before even though we’re working with a UserDecorator instead of a User. Now we can start to clean up our views and we’ll begin with the code that renders the user’s avatar.

/app/views/users/show.html.erb

<%= link_to_if @user.url.present?, image_tag( ↵ 
  "avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>

We’ll replace this in the view with this:

/app/views/users/show.html.erb

<%= @user.avatar %>

This code will look for an avatar method in the UserDecorator which we’ll write next. There are some things we’ll need to be aware of when writing this method. Whenever we call a helper method from a decorator, such as our link_to_if method, we need to call it through the h method (this stands for “helpers”). When we want to reference the model we call model instead of, in this case, @user.

The code we’ve copied from the the view into avatar calls the avatar_name helper method. As we’re calling avatar_name from our decorator we’ll move it there from the UsersHelper class. Now that we have the method in the same class we don’t need to pass it a User and we can replace its calls to user with model.

/app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user
  def avatar
    h.link_to_if model.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), model.url
  end
  private
  def avatar_name
    if model.avatar_image_name.present?
      model.avatar_image_name
    else
      "default.png"
    end
  end
end

Next we’ll tidy up the code that render’s the user’s name. We’ll replace this code in the view:

/app/views/users/show.html.erb

<h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>

with this:

/app/views/users/show.html.erb

<h1><%= @user.linked_name %></h1>

We’ll need to write the linked_name method in the UserDecorator. There are similarities between the code we’ve taken from the template and the avatar method we wrote earlier; both of these render a link whose content is dependent on whether the user’s url if it’s present. As we’re in a class it’s easy to refactor this duplication out.

To handle the link creation we’ll create a new private method called site_link, which takes the content as a parameter. We can then call this method in both the avatar and linked_name methods to tidy them up. As before we replace any calls to @user in the linked_name with model. With all that done our decorator now looks like this.

app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user
  def avatar
    site_link h.image_tag("avatars/#{avatar_name}", ↵
      class: "avatar")
  end
  def linked_name
    site_link(model.full_name.present? ? model.full_name : ↵
      model.username)
  end
  private
  def site_link(content)
    h.link_to_if model.url.present?, content, model.url
  end
  def avatar_name
    if model.avatar_image_name.present?
      model.avatar_image_name
    else
      "default.png"
    end
  end
end

If we reload a user’s profile page now it should look just as it did before.

Our template is already looking a lot cleaner but there’s more we can do yet. Next we’ll refactor a larger chunk of the view code, the code that renders a link to the user’s website.

/app/views/user/show.html.erb

<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 this with:

/app/views/user/show.html.erb

<dt>Website:</dt>
<dd><%= @user.website %></dd>

As before we’ll create a method in the decorator class. We can see from the code that we’ve removed from the view that if the user has no url then some HTML is rendered. We could just return this as a string but we don’t want to put raw HTML into a Ruby string. Another solution would be to move the code into a partial and to render that, but as we only to output a single HTML element it makes more sense to use the content_tag helper method.

/app/decorators/user_decorator.rb

def website
  if model.url.present?
    h.link_to model.url, model.url
  else
    h.content_tag :span, "None given", class: "none"
  end  
end

We can do a similar thing for the two parts of the template that render the Twitter information and the user’s biography. We won’t show the details of that here, but after we’ve made the changes our view code will looks a lot cleaner.

/app/views/users/show.html.erb

<div id="profile">
  <%= @user.avatar %>
  <h1><%= @user.linked_name %></h1>
  <dl>
    <dt>Username:</dt>
    <dd><%= @user.username %></dd>
    <dt>Member Since:</dt>
    <dd><%= @user.member_since %></dd>
    <dt>Website:</dt>
    <dd><%= @user.website %></dd>
    <dt>Twitter:</dt>
    <dd><%= @user.twitter %></dd>
    <dt>Bio:</dt>
    <dd><%= @user.bio %></dd>
  </dl>
</div>

The new twitter and bio methods in the decorator look like this:

/app/decorators/user_decorator.rb

def website
  if model.url.present?
    h.link_to model.url, model.url
  else
    h.content_tag :span, "None given", class: "none"
  end  
end
def twitter
  if model.twitter_name.present?
    h.link_to model.twitter_name, ↵  
      "http://twitter.com/#{model.twitter_name}"
  else
    h.content_tag :span, "None given", class: "none"
  end
end
def bio
  if model.bio.present?
    Redcarpet.new(model.bio, :hard_wrap, :filter_html, ↵
 		:autolink).to_html.html_safe
  else
    h.content_tag :span, "None given", class: "none"
  end
end

The two new methods look very similar to each other and also to the website method we wrote earlier. There’s a fair amount of duplication between the three methods, especially in each else clause, so it would be good if we could extract this part out into its own method.

We can use a block to help with this. We’ll extract the else clause out into its own method which we’ll call handle_none. We’ll pass the value we want to check the presence of to this method and also a block. If the value is present the code in the block will be executed, otherwise the span tag will be rendered. We can then use this handle_none to tidy up the website, twitter and bio methods.

/app/decorators/user_decorator.rb

def website
  handle_none model.url do
    h.link_to model.url, model.url
  end  
end
def twitter
  handle_none model.twitter_name do
    h.link_to model.twitter_name, ↵ 
      "http://twitter.com/#{model.twitter_name}"
  end
end
def bio
  handle_none model.bio do
    Redcarpet.new(model.bio, :hard_wrap, :filter_html, ↵
      :autolink).to_html.html_safe
  end
end
private
def handle_none(value)
  if value.present?
    yield
  else
    h.content_tag :span, "None given", class: "none"
  end
end

Another change we could make it to extract the Markdown rendering into the ApplicationDecorator so that we can call it from any other decorators we might make. We’ll create a new markdown method there now that will render any text we pass to it.

/app/decorators/application_decorator.rb

class ApplicationDecorator < Draper::Base
  def markdown(text)
    Redcarpet.new(text, :hard_wrap, :filter_html, ↵ 
      :autolink).to_html.html_safe
  end
end

Now in the UserDecorator we can now modify the bio method so that it calls markdown.

/app/decorators/user_decorator.rb

def bio
  handle_none model.bio do
    markdown(model.bio)
  end
end

Modifying The Model

Now that we have the decorator in place it’s a good idea to look through the model layer for any view-related code that we can move up to the relevant decorator. For example in our User model we have a member_since method that formats the user’s created_at time. This code can be considered view-related as all it does is return a formatted string so we’ll move it to the decorator.

/app/models/user.rb

class User < ActiveRecord::Base
  def member_since
    created_at.strftime("%B %e, %Y")
  end
end

All we need to do is move the method to the decorator and prepend model before created_at.

/app/decorators/user_decorator.rb

def member_since
  model.created_at.strftime("%B %e, %Y")
end

Restricting Access To The Model With The allows Method

While we’re modifying the UserDecorator there’s one more feature of Draper that we’ll demonstrate: the allows method. As it stands the UserDecorator will delegate all of its methods to the User object, but we can choose which methods are delegated to the User model by using allows and passing it the name of the methods we want to delegate.

/app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user
  allows :username
  # Other methods omitted
end

We’ll allow only username to be delegated and this way only the username method will be delegated down to the User model. This is the only method we need to delegate as it’s the only method that’s called in the view that doesn’t come from the decorator. This gives us more control over the decorator’s interface.

Now that we’re done with refactoring everything out to the decorator we’ll try loading a user’s profile page again to make sure that everything still looks the same.

The full user profile page looks the same after our changes.

It does. We can even check our other user and that still looks the same too but our view code is much cleaner.

The profile page for MrMystery is unchanged, too.

By using a decorator our show template has been reduced from 1050 bytes in 34 lines to 382 bytes in 16 lines, a reduction in size of almost two thirds. It looks much cleaner too and we’ve made it much easier to edit should we want to change the layout of the page.