286: Draper
(view original Railscast)
Other translations:
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 page seems fairly simple but we also have to handle those users who haven’t entered so much data such as “MrMystery”.
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.
It does. We can even check our other user and that still looks the same too but our view code is much cleaner.
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.