Opinionated presenters for Rails 5 - without the cruft
Ruby HTML
Latest commit 90e2878 Oct 12, 2016 @endofunky Bump to v0.3.0
Permalink
Failed to load latest commit information.
lib
tasks Initial commit Aug 16, 2016
test Added support for singleton classes Oct 6, 2016
.gitignore Delegate to controller's most recent view context Aug 20, 2016
.travis.yml
CHANGELOG.md Update CHANGELOG [ci skip] Oct 7, 2016
CONTRIBUTING.md
Gemfile Initial commit Aug 16, 2016
LICENSE
README.md Update README [ci skip] Sep 7, 2016
Rakefile Initial commit Aug 16, 2016
oprah.gemspec Fix license in gemspec Aug 22, 2016

README.md

Oprah

Gem Version Build Status Code Climate Dependency Status

Opinionated presenters for Rails 5 - without the cruft.

Table of Contents

Overview

If you've ever worked on a sufficiently large Rails application you've probably experienced the Rails helper mess first hand. Helper methods are annoying to locate, hard to test and not terribly expressive.

So why another presenter/decorator library? Oprah was written with a few simple goals in mind only covered partially (or not at all) by other gems:

  • Thin, lightweight layer over Ruby's SimpleDelegator
  • Presenters should be easy to test
  • Avoid monkey patching, where possible 🐒🔫
  • Embrace convention over configuration
  • First-class support for composition (modules and concerns)

Installation

Add this line to your application's Gemfile:

gem 'oprah'

And then execute:

$ bundle

Getting started

Oprah expects a single presenter for each of your classes or modules. If your model is called User it will look for a class called UserPresenter:

class User
  def first_name
    "John"
  end

  def last_name
    "Doe"
  end
end

class UserPresenter < Oprah::Presenter
  def name
    "#{first_name} #{last_name}"
  end
end

Oprah will figure out the presenters by itself so you don't have to instantiate your presenter classes directly:

presenter = Oprah.present(User.new)

presenter.name
# => "John Doe"

Of course, all the regular methods on your model are still accessible:

presenter.first_name
# => "John"

If you DO want to use a specific presenter, you can simply instantiate it yourself:

SomeOtherPresenter.new(User.new)

ActionController integration

Now, where do we put our presenters? Ideally, you'd want to expose them in your controller. Oprah avoids monkey patching and generally it's good to be aware of what's going on, even if that means to be (at least a little bit) explicit.

Here's how you can use Oprah presenters from your controller:

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

This will also take care of passing the correct view context to the presenter, which you can access with the #view_context (or shorter, #h) instance method.

ActionMailer integration

Oprah will make the same helpers you have in ActionController available to ActionMailer:

class UserMailer < ApplicationMailer
  default from: 'notifications@example.com'

  def welcome_email(user)
    @user = present user
    mail(to: @user.email, subject: 'Welcome to My Awesome Site')
  end
end

Collections

Oprah has basic support for collections with .present_many. It will simply apply it's .present behavior to each object in the given collection:

users = [User.new, User.new]
presenters = Oprah.present_many(users)

presenters.first.kind_of?(UserPresenter)
# => true

presenters.last.kind_of?(UserPresenter)
# => true

Of course, this works in controllers, too:

class UserController < ApplicationController
  def index
    @users = present_many User.all
  end
end

Associations

You can also automatically use presenters for your associations using the #presents_one and #presents_many macros. Let's say you have the following Project model:

class Project
  has_many :users
  has_one :owner, class_name: "User"
end

Oprah lets you easily wrap the associated objects:

class ProjectPresenter < Oprah::Presenter
  presents_many :users
  presents_one :owner
end

Note that you don't need to explicitly state the association class.

Composition

Let's say you extraced some behaviour out of your model into a reusable module (or ActiveSupport::Concern). Oprah lets you write a single, separate presenter for this module and automatically chains it to your "main presenter" by walking up the ancestor chain of the given object.

Let's say we want to mix a shared Describable module into our User class from above and render the description to HTML:

module Describable
  def description
    "*AWESOME*"
  end
end

class User
  include Describable
end

class DescribablePresenter < Oprah::Presenter
  def description
    Kramdown::Document.new(object.description).to_html
  end
end

You can now access the methods of both, UserPresenter and DescribablePresenter:

presenter = Oprah.present(User.new)

presenter.description
=> "<p><em>AWESOME</em></p>\n"

presenter.name
# => John Doe

Performance

Of course, looking up all the presenters would imply a performance issue. But don't worry, Oprah caches all matching presenters for a class (and busts it's cache on code reloads for a smooth development experience).

Ordering

Oprah walks your object's ancestor chain in reverse. For example, you'd be able to access the methods exposed by the DescribablePresenter from your UserPresenter. You can even use super:

class DescribablePresenter < Oprah::Presenter
  def baz
    "foo"
  end
end

class UserPresenter < Oprah::Presenter
  def baz
    super + "bar"
  end
end

Oprah.present(User.new).baz
# => "foobar"

Choosing presenters

When presenting an object you can optionally choose which presenter classes to use:

Oprah.present(User.new, only: DescribablePresenter)

This parameter takes either a single presenter or an Array of presenters. The presenter(s) given need to match the object's class or one of it's ancestors. Non-matching presenters given will be ignored.

Testing

Testing presenters is as simple as testing a regular class. Oprah also provides couple of helpers to make it even easier:

class UserPresenterTest < Minitest::Test
  include Oprah::TestHelpers

  def setup
    @presenter = present User.new
  end

  def test_presented
    assert_presented @presenter
  end

  def test_name
    assert_equal "John Doe", @presenter.name
  end
end

API Documentation

Comprehensive API Documentation is available at rubydoc.info.

Contributing

Please check out our contributing guidelines.

License

Released under the MIT license. See the LICENSE file for details.

Author

Tobias Svensson, @endofunky, http://github.com/endofunky