Resubject

Build Status Gem Version Code Climate

Uber simple presenters using Ruby's SimpleDelegator.

Installation

Add this line to your application's Gemfile:

gem 'resubject', '~> 0.2.2'

And then execute:

$ bundle

Ruby/Rails versions

This gem is compatible with Ruby 1.9 and 2.0 and Rails 3 and 4.

Documentation

Checkout the documentation in rdoc.info/resubject

Introduction

Resubject uses Ruby's SimpleDelegator class to create its presenters.

SimpleDelegator is a concrete implementation of the Delegator class. Basically, it delegates any method calls to the object passed into the constructor:

require 'delegate'

array = SimpleDelegator.new([1, 2, 3])

array.count # => 3
array.map   # => #<Enumerator: ...>

It doesn't override the class name, but you still can access the original object.

array.class
# => SimpleDelegator
array.__getobj__.class
# => Array

This means you can create a class that inherits from SimpleDelegator and customize its behaviour:

class ForeverZeroArray < SimpleDelegator
  def omg!
    "OMG!"
  end

  def count
    0
  end
end

You can define new methods or override existing ones:

ForeverZeroArray.new([1,2,3]).count
# => 0
ForeverZeroArray.new([1,2,3]).omg!
# => OMG!

Usage

Using the Resubject::Presenter class, you can create Presenters from it. Example:

class Box < Struct.new(:name, :items)
end

class BoxPresenter < Resubject::Presenter
  def contents
    items.join(', ')
  end
end

Then use the delegator:

box = Box.new('Awkward Package', ['platypus', 'sloth', 'anteater'])

presentable = BoxPresenter.new(box)
presentable.contents
# => platypus, sloth, anteater

If you have a collection of objects and want to add a presenter to each one, use Resubject.all

boxes = [box1, box2, box3]
BoxPresenter.all boxes
# => [<BoxPresenter>, <BoxPresenter>, <BoxPresenter>]

Rails

If you're using rails, Resubject automatically includes helpers in your controllers and views

class BoxesController < ActionController::Base
  def index
    @boxes = present Boxes.all
  end
end

The #present method will automatically identify the presenter class by the object's class name. If you want to customize the class:

def index
  @boxes = present Boxes.all, SpecialBoxPresenter
end

It also accepts multiple presenters:

def index
  @boxes = present Boxes.all, BoxPresenter, ExtendedBoxPresenter
end

Or if you prefer, you can use the #present method directly into your views

<%= present(@box).contents %>

Helpers

You can define presentable attributes:

class PostPresenter < Resubject::Presenter
  presents :title
  presents :comments # or => presents :comments, CommentPresenter
end

Then the attributes will return an instance of those presenters:

post.title
# => <TitlePresenter>

post.comments
# => [<CommentPresenter>, <CommentPresenter>, <CommentPresenter>]

Or if you wish, you can use the present method inside your class

class PostPresenter < Resubject::Presenter
  def comments
    present(to_model.comments, SomePresenter)
  end
end

Helpers on Rails

Resubject can generate some rails helpers for your attributes:

class ProductPresenter < Resubject::Presenter
  currency :price, precision: 2
end

Will generate:

product.price
# => $10.00

Available helpers

Presenter           Maps to                     Class/Module

currency            number_to_currency          ActionView::Helpers::NumberHelper
percentage          number_to_percentage        ActionView::Helpers::NumberHelper
time_ago            time_ago_in_words           ActionView::Helpers::DateHelper
date_format         to_s                        ActiveSupport::TimeWithZone

More helpers will be added. Feel free to contribute with yours! Also, Check out the extensions file.

Helpers without rails

If you'd like to use the generated helpers but you're not using Rails, you can either use ActionView or create your own template handler:

require 'action_view'

post = PostPresenter.new(post, ActionView::Base.new)
# Or
post = PostPresenter.new(post, MyTemplateHandler.new)

If you want to use your own template handler and still use Resubject helpers, you may want to define the same ActionView helpers in your handler (or only the ones you will actually use).

Testing

Resubject introduces a new context for RSpec that helps testing your presenters:

# spec/presenters/my_presenter_spec.rb
# require 'action_view' # require this first if you want to test action view helpers
require 'resubject/rspec'
require 'presenters/my_presenter'

describe UserPresenter do
  let(:object) { mock :user }

  it 'has full name' do
    object.stub(first: 'User', last: 'Name')
    expect(subject.name).to eq 'User Name'
  end
end

By placing the file into spec/presenters, Resubject automatically includes the subject and template variables into your spec, so you don't need to define them on every spec.

NOTE: Please note that the presenter is tested on isolation. It's not required but very recommended.

Maintainers

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request