decorators_on_rails



decorators_on_rails

0 2


decorators_on_rails

A lightning talk on tidying up view logic in Rails apps using the Decorator Pattern and Draper gem.

On Github johnotander / decorators_on_rails

Decorators on Rails

John Otander / johnotander.com

https://github.com/johnotander/draper_example https://github.com/drapergem/draper http://johnotander.com/rails/2014/03/07/decorators-on-rails/

What is a Decorator?

A Decorator is:

A design pattern that allows behavior to be added to an individual object without affecting the behavior of other objects from the same class.

A disclaimer

Don't use Decorators, or Presenters, until you're in pain. Your models should be bursting at the seams.

Why do we care?

Your views should be stupid.

Steve Klabnik said it perfectly:

The whole idea of logic in templates leads to all kinds of problems. They're hard to test, they're hard to read, and it's not just a slippery slope, but a steep one. Things go downhill rapidly.

Some view code.

	  <h1>Show user</h1>

<dl class="dl-horizontal">
  <% if @user.public_email %>
    <dt>Email:</dt>
    <dd><%= @user.email %></dd>
  <% else %>
    <dt>Email Unavailable:</dt>
    <dd><%= link_to 'Request Email', '#', class: 'btn btn-default btn-xs' %></dd>
  <% end %>

  <dt>Name:</dt>
  <dd>
    <% if @user.first_name || @user.last_name %>
      <%= "#{ @user.first_name } #{ @user.last_name }".strip %>
    <% else %>
      No name provided.
    <% end %>
  </dd>
  
  <dt>Joined:</dt>
  <dd><%= @user.created_at.strftime("%A, %B %e") %></dd>
  
  <!-- ... -->
  
</dl>
	

Some model code.

    class User < ActiveRecord::Base
  before_create :setup_analytics
  after_create :send_welcome_email
  after_save :send_confirmation_email, if: :email_changed?

  validates :website, url_format: true
  validates :email, email_format: true, 
                    presence: { message: "Please specify an email." }, 
                    uniqueness: { case_sensitive: false, 
                                  message: "That email has already been registered." }

  validates_presence_of :password

  default_scope -> { order(:last_name, :first_name) }

  def full_name
    if first_name.blank? && last_name.blank?
      'No name provided.'
    else
      "#{ first_name } #{ last_name }".strip
    end
  end

  def as_json(options = {})
    json_blob = super
    json_blob.delete(:email) unless public_email
    json_blob
  end

  def email_domain
    email.split(/@/).second
  end

  def website_domain
    UrlFormat.get_domain(url)
  end

  private

    def send_confirmation_email
      # ...
    end

    def send_welcome_email
      # ...
    end

    def setup_analytics
      # ...
    end
end
  

Draper to the rescue.

gem 'draper'

$ bundle install

$ rails generate decorator User

The default decorator.

Found in app/decorators/user_decorator.rb

    class UserDecorator < Draper::Decorator
  delegate_all
end
  

Let's add some specs.

    require 'spec_helper'

describe UserDecorator do

  let(:first_name) { 'John'  }
  let(:last_name)  { 'Smith' }

  let(:user) { FactoryGirl.build(:user, 
                                 first_name: first_name, 
                                 last_name: last_name) }
  
  let(:decorator) { user.decorate }

  describe '.full_name' do

    context 'without a first name' do

      before { user.first_name = '' }

      it 'should return the last name' do
        expect(decorator.full_name).to eq(last_name)
      end
    end

    context 'with a first and last name' do

      it 'should return the full name' do
        expect(decorator.full_name).to eq("#{ first_name } #{ last_name }")
      end
    end

    context 'without a first or last name' do

      before do
        user.first_name = ''
        user.last_name = ''
      end

      it 'should return no name provided' do
        expect(decorator.full_name).to eq('No name provided.')
      end
    end
  end
end
  

Implementing the decorator.

    class UserDecorator < Draper::Decorator
  delegate_all

  def email_or_request_button
    public_email ? email : h.link_to('Request Email', '#', class: 'btn btn-default btn-xs').html_safe
  end

  def full_name
    if first_name.blank? && last_name.blank?
      'No name provided.'
    else
      "#{ first_name } #{ last_name }".strip
    end
  end

  def joined_at
    created_at.strftime("%B %Y")
  end
end
  

Now we need to decorate.

    class UsersController < ApplicationController
  before_action :do_stuff

  # GET /users
  # GET /users.json
  def index
    @users = User.all.decorate
  end

  # GET /users/1
  # GET /users/1.json
  def show
    @user = User.find(params[:id]).decorate
  end
end
  

The original view.

    <h1>Show user</h1>

<dl class="dl-horizontal">
  <% if @user.public_email %>
    <dt>Email:</dt>
    <dd><%= @user.email %></dd>
  <% else %>
    <dt>Email Unavailable:</dt>
    <dd><%= link_to 'Request Email', '#', class: 'btn btn-default btn-xs' %></dd>
  <% end %>

  <dt>Name:</dt>
  <dd>
    <% if @user.first_name || @user.last_name %>
      <%= "#{ @user.first_name } #{ @user.last_name }".strip %>
    <% else %>
      No name provided.
    <% end %>
  </dd>
  
  <dt>Joined:</dt>
  <dd><%= @user.created_at.strftime("%A, %B %e") %></dd>
  
  <!-- ... -->
  
</dl>
  

The new, decorated view.

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

<dl class="dl-horizontal">
  <dt><%= @user.email_attr_text %></dt>
  <dd><%= @user.email_or_request_button %></dd>

  <dt>Name:</dt>
  <dd><%= @user.full_name %></dd>
  
  <dt>Joined:</dt>
  <dd><%= @user.joined_at %></dd>
  
  <!-- ... -->
  
</dl>
  

Kthxbai.

John Otander / johnotander.com

Resources