Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
The unobtrusive presenter pattern for Rails
Ruby
tag: v0.0.1

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
lib
spec
.gitignore
Gemfile
LICENSE
README.md
Rakefile
liaison.gemspec

README.md

Liaison

A Rails presenter class.

A major idea of the presenter pattern is to break off the business logic from the view object, letting the view logic be a dumb instance that knows how to get, set, and validate values. The business logic can then query the presenter object for the values as needed.

Look, here's an example business object:

class SignUp
  attr_reader :user

  def initialize(presenter, account_builder = Account)
    @email        = presenter[:email]
    @password     = presenter[:password]
    @account_name = presenter[:account_name]

    @presenter       = presenter
    @account_builder = account_builder
  end

  def save
    if presenter.valid?
      account = account_builder.new(:name => account_name)
      @user = account.users.build(:email => email, :password => password)
      account.save.tap do |succeeded|
        presenter.add_errors(account.errors) unless succeeded
      end
    end
  end

  protected

  attr_accessor :email, :password, :account_name, :account_builder, :presenter
end

It's just a class, which you can unit test as you please. A presenter object is passed in, then we pull the values out, make sure it's valid, and add errors to it as needed. This class does not deal directly with validations, state, or any of the ActiveModel nonsense.

Now you need to know how to use a Presenter object, so this is what the controller looks like:

class SignupsController < ApplicationController
  def new
    @sign_up = presenter
  end

  def create
    @sign_up = presenter.with_params(params[:sign_up])
    db = SignUp.new(@sign_up)

    if db.save
      sign_in_as(db.user)
      redirect_to root_url
    else
      render :new
    end
  end

  protected

  def presenter
    Presenter.new('sign_up',
                  :fields => [:email, :password, :account_name],
                  :validator => SignUpValidator.new)
  end
end

In our new action we simply set the @sign_up i-var to an instance of the Presenter. In create we use that Presenter instance, adding CGI params in. Then we pass that to the SignUp class defined above and it's all boring from there.

The presenter method in the above example produces a new Presenter instance. This instance has a model name (sign_up), fields the form will handle (email, password, and account_name), and a validator (SignUpValidator). The validator is any instance of ActiveModel::Validator, for example:

class SignUpValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, "can't be blank") if value.blank?
  end
end

You, the author of the business logic class, are in charge of checking in on these validations and errors. For example, before saving any objects you should check Presenter#valid?. And after you've saved something to the database you should add any errors onto the presenter using Presenter#add_errors.

Getting Data

An instance of the Presenter object is Hash-like: it implements the Enumerable module, which means it has an #each method among many others; it also has a #[] method, which you can use to access values just like with the CGI params hash.

Testing

When writing your unit tests it'll be handy to have a mock presenter around, which is why we package a MockPresenter class for you to use. It gives you access to the #have_errors and #have_no_errors RSpec matchers.

describe SignUp, 'invalid' do
  let(:params) { { :email => '',
                   :password => 'bar',
                   :account_name => 'baz' } }
  let(:errors) { { :email => "can't be blank" } }
  let(:presenter) do
    MockPresenter.new(:valid => false,
                      :params => params,
                      :errors => errors)
  end
  let(:account_builder) { MockAccount.new(:valid => true) }

  subject { SignUp.new(presenter, account_builder) }

  it "does not save the account or user" do
    subject.save.should be_false

    presenter.should have_errors(errors)
  end
end

Contact

Copyright 2011 Mike Burns.

Please open a pull request on Github as needed.

Something went wrong with that request. Please try again.