Uber simple presenters using Ruby's SimpleDelegator.
Add this line to your application's Gemfile:
gem 'resubject', '~> 0.3.0'
And then execute:
$ bundle
This gem is compatible with Ruby 1.9 and 2.0 and Rails 3 and 4.
Checkout the documentation in rdoc.info/resubject
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!
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>]
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 %>
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
Resubject
can generate some rails helpers for your attributes:
class ProductPresenter < Resubject::Presenter
currency :price, precision: 2
end
Will generate:
product.price
# => $10.00
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.
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).
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) { double :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.
- Felipe Elias Philipp - coderwall.com/felipeelias
- Piotr Jakubowski - coderwall.com/piotrj
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request