An opinionated presentation layer comprising presenters and formatters.
Chic was borne out of a need for a simple PORO-style presentation layer for Rails applications to help DRY up formatting logic in views and view helpers.
To get started, add this line to your Gemfile and install it using Bundler:
gem 'chic'
Though it is not a requirement, for ease of reference it is assumed that you will be using this library in a Rails application where you will be creating presenters for your models.
Create a presenter by deriving from Chic::Presenter
, and then declare which attributes should return formatters or presenters.
# app/presenters/foo_presenter.rb
class FooPresenter < Chic::Presenter
formats :baz
presents bar: 'BarPresenter'
end
Include Chic::Presentable
to make your objects presentable:
# app/models/foo.rb
class Foo < ApplicationRecord
include Chic::Presentable
belongs_to :bar
end
Instantiate a presenter by calling .present
on the presenter class, for example in a Rails view:
<% FooPresenter.present @foo do |foo_presenter| %>
<!-- ... -->
<% end %>
Collections can be presented using .present_each
:
<% FooPresenter.present_each @foos do |foo_presenter, foo| %>
<!-- ... -->
<% end %>
You can also include the view helpers:
module ApplicationHelper
include Chic::Helpers::View
end
Which will allow you to instantiate presenters without having to use the class name:
<% present @foo do |foo_presenter| %>
<!-- ... -->
<% end %>
<% present_each @foos do |foo_presenter, foo| %>
<!-- ... -->
<% end %>
See the Conventions section below for more on using presenters.
Formatters format values by deriving from Chic::Formatters::Nil
and overriding the private value
method:
# app/formatters/date_time_formatter.rb
class DateTimeFormatter < Chic::Formatters::Nil
private
def value
return if object.blank?
object.strftime('%-d %b %Y %H:%M')
end
end
Note: You should always return nil
if the object being formatted is blank so that the Nil
formatter behaves correctly.
Provide additional formatter options as chainable methods:
# app/formatters/date_time_formatter.rb
class DateTimeFormatter < Chic::Formatters::Nil
def format=(value)
@format = value
self
end
private
def value
return if object.blank?
object.strftime(@format || '%-d %b %Y %H:%M')
end
end
Declare formatted values in presenters using formats
:
# app/presenters/foo_presenter.rb
class FooPresenter < Chic::Presenter
formats :created_at,
with: 'DateTimeFormatter'
end
Render formatted values by calling #to_s
on the formatter returned, which happens implicitly in Rails views for example:
<% FooPresenter.present @foo do |foo_presenter, _foo| %>
<%= foo_presenter.created_at %>
<% end %>
If the formatter derives from Chic::Formatters::Nil
, then configure a blank value to be used:
# app/presenters/foo_presenter.rb
class FooPresenter < Chic::Presenter
formats :created_at,
with: 'DateTimeFormatter',
options: {
blank_value: '(Not yet created)'
}
end
If the formatter supports additional options using chainable methods as described above, configure those options in the same way:
# app/presenters/foo_presenter.rb
class FooPresenter < Chic::Presenter
formats :created_at,
with: 'DateTimeFormatter',
options: {
format: '%-d %B %Y at %H:%M'
}
end
If needed, override those options where the formatted value is being rendered:
<% FooPresenter.present @foo do |foo_presenter, _foo| %>
<%= foo_presenter.created_at.format('%c').blank_value('–') %>
<% end %>
Optionally configure formatters with a name, for example in a Rails initializer:
# config/initializers/chic.rb
require 'chic'
Chic.configure do |config|
config.formatters.merge!(
date_time: 'DateTimeFormatter'
)
end
Allowing you to refer to those formatters by name instead of by class:
# app/presenters/foo_presenter.rb
class FooPresenter < Chic::Presenter
formats :created_at,
with: :date_time
end
If a presenter class for an object you're trying to present can't be found, an entry at debug level will be made to the configured logger.
You can configure the logger to be used:
# config/initializers/chic.rb
require 'chic'
Chic.configure do |config|
config.logger = Logger.new($stdout)
end
It may be beneficial to know about missing presenter classes sooner than later, in which case you can enable exceptions when it makes sense to do so – for example, a Rails application in any environment other than production:
# config/initializers/chic.rb
require 'chic'
Chic.configure do |config|
config.raise_exceptions = Rails.env.production? == false
end
A few helpful conventions that have gone a long way to keep things maintainable.
Presenter class names are derived by appending Presenter
to the #model_name
or the class name of the object being presented. It is strongly recommended that you stick to this convention, but if you need to change it – for example you might have overridden #model_name
– you can do so by defining a #presenter_class
method:
# app/forms/user/sign_up_form.rb
class User::SignUpForm < User
include Chic::Presentable
def self.model_name
ActiveModel::Name.new(self, nil, 'User')
end
def presenter_class
User::SignUpFormPresenter
end
end
Try not instantiate presenters outside of the view layer if possible.
Don't
# app/controllers/foo_controller.rb
class FoosController < ApplicationController
def show
@foo = Foo.find(params[:id])
@foo_presenter = FooPresenter.new(@foo)
end
end
<!-- app/views/foos/_show.html.erb -->
<%= link_to @foo_presenter.created_at, foo_path(@foo) %>
Do
# app/controllers/foo_controller.rb
class FoosController < ApplicationController
def show
@foo = Foo.find(params[:id])
end
end
<!-- app/views/foos/_show.html.erb -->
<% present @foo do |foo_presenter| %>
<%= link_to foo_presenter.created_at, foo_path(@foo) %>
<% end %>
It can get messy if you pass presenter instances to other views, try avoid that if possible.
Don't
<% present @foo do |foo_presenter| %>
<!-- ... -->
<%= render partial: 'path/to/partial', locals: { foo: foo_presenter } %>
<% end %>
Do
<% present @foo do |foo_presenter| %>
<!-- ... -->
<%= render partial: 'path/to/partial', locals: { foo: @foo } %>
<% end %>
It may be tempting to use presenters for conditional logic, but it's far better to use the original object for anything other than presentation.
Don't
<% present_each @foos do |foo_presenter, foo| %>
<% if foo_presenter.created_at %>
<%= link_to foo_presenter.created_at, foo_path(foo_presenter) %>
<% end
<% end %>
Note: Passing a presenter instance to a route helper as shown would only work if the presenter declared id
as a formatted attribute. While this may work, it is not predictable behaviour.
Do
<% present_each @foos do |foo_presenter, foo| %>
<% if foo.created_at %>
<%= link_to foo_presenter.created_at, foo_path(foo) %>
<% end
<% end %>
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/collcoll/chic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Chic project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.