README.md in chic-0.2.0 vs README.md in chic-0.3.0

- old
+ new

@@ -1,87 +1,355 @@ # Chic -Opinionated presentation layer comprised of presenters and formatters. +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. + ## Installation -Add this line to your application's Gemfile: +To get started, add this line to your Gemfile and install it using Bundler: - gem 'chic' +```ruby +gem 'chic' +``` -And then execute: +## Usage - $ bundle +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. -Or install it yourself as: +### Creating Presenters - $ gem install chic +Create a presenter by deriving from `Chic::Presenter`, and then declare which attributes should return formatters or presenters. -## Usage +```ruby +# app/presenters/foo_presenter.rb -### Being Presentable +class FooPresenter < Chic::Presenter + formats :baz + + presents bar: 'BarPresenter' +end +``` -Make objects easily presentable by: +### Using Presenters +Include `Chic::Presentable` to make your objects presentable: + ```ruby -class Foo +# app/models/foo.rb + +class Foo < ApplicationRecord include Chic::Presentable + + belongs_to :bar end ``` -### Creating Presenters +Instantiate a presenter by calling `.present` on the presenter class, for example in a Rails view: -Present presentables with a presenter by inheriting from `Chic::Presenter`: +```erb +<% FooPresenter.present @foo do |foo_presenter| %> + <!-- ... --> +<% end %> +``` +Collections can be presented using `.present_each`: + +```erb +<% FooPresenter.present_each @foos do |foo_presenter, foo| %> + <!-- ... --> +<% end %> +``` + +You can also include the view helpers: + ```ruby -class FooPresenter < Chic::Presenter - # ... +module ApplicationHelper + include Chic::Helpers::View end ``` -You can also include `Chic::Presents` and `Chic::Formats` as needed: +Which will allow you to instantiate presenters without having to use the class name: +```erb +<% present @foo do |foo_presenter| %> + <!-- ... --> +<% end %> + +<% present_each @foos do |foo_presenter, foo| %> + <!-- ... --> +<% end %> +``` + +See the [Conventions](#conventions) section below for more on using presenters. + +### Creating Formatters + +Formatters format values by deriving from `Chic::Formatters::Nil` and overriding the private `value` method: + ```ruby -class FooPresenter - include Chic::Formats - include Chic::Presents - # ... +# 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 need to make sure that the object being presented and the context in which it is being presented, for example the view, are accessible through `object` and `context` on the presenter instance respectively. +**Note:** You should always return `nil` if the object being formatted is blank so that the `Nil` formatter behaves correctly. -### Using Presenters +Provide additional formatter options as chainable methods: -Presenters should be instantiated from views using `.present`: +```ruby +# 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 +``` + +### Using Formatters + +Declare formatted values in presenters using `formats`: + ```ruby +# 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: + +```erb <% FooPresenter.present @foo do |foo_presenter, _foo| %> - <!-- ... --> + <%= foo_presenter.created_at %> <% end %> ``` -Collections can be presented using `.present_each`: +#### Configurable options +If the formatter derives from `Chic::Formatters::Nil`, then configure a blank value to be used: + ```ruby -<% FooPresenter.present_each @foos do |foo_presenter, _foo| %> - <!-- ... --> +# 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: + +```ruby +# 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: + +```erb +<% FooPresenter.present @foo do |foo_presenter, _foo| %> + <%= foo_presenter.created_at.format('%c').blank_value('–') %> <% end %> ``` -If you've made use of the view helpers, you can drop the class name: +#### Named formatters +Optionally configure formatters with a name, for example in a Rails initializer: + ```ruby -<% present @foo do |foo_presenter, _foo| %> +# 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: + +```ruby +# app/presenters/foo_presenter.rb + +class FooPresenter < Chic::Presenter + formats :created_at, + with: :date_time +end +``` + +## Logging + +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: + +```ruby +# 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: + +```ruby +# config/initializers/chic.rb + +require 'chic' + +Chic.configure do |config| + config.raise_exceptions = Rails.env.production? == false +end +``` + +## Conventions + +A few helpful conventions that have gone a long way to keep things maintainable. + +### Naming presenter classes + +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: + +```ruby +# 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 +``` + +### Instantiate presenters in views only + +Try not instantiate presenters outside of the view layer if possible. + +**Don't** + +```ruby +# app/controllers/foo_controller.rb + +class FoosController < ApplicationController + def show + @foo = Foo.find(params[:id]) + @foo_presenter = FooPresenter.new(@foo) + end +end +``` + +```erb +<!-- app/views/foos/_show.html.erb --> + +<%= link_to @foo_presenter.created_at, foo_path(@foo) %> +``` + +**Do** + +```ruby +# app/controllers/foo_controller.rb + +class FoosController < ApplicationController + def show + @foo = Foo.find(params[:id]) + end +end +``` + +```erb +<!-- app/views/foos/_show.html.erb --> + +<% present @foo do |foo_presenter| %> + <%= link_to foo_presenter.created_at, foo_path(@foo) %> +<% end %> +``` + +### Keep presenter instances scoped to only the view in which they're being used + +It can get messy if you pass presenter instances to other views, try avoid that if possible. + +**Don't** + +```erb +<% present @foo do |foo_presenter| %> <!-- ... --> + <%= render partial: 'path/to/partial', locals: { foo: foo_presenter } %> <% end %> ``` -And: +**Do** -```ruby -<% present_each @foo do |foo_presenter, _foo| %> +```erb +<% present @foo do |foo_presenter| %> <!-- ... --> + <%= render partial: 'path/to/partial', locals: { foo: @foo } %> +<% end %> +``` + +### Use presenters and formatters for presentation only + +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** + +```erb +<% 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** + +```erb +<% present_each @foos do |foo_presenter, foo| %> + <% if foo.created_at %> + <%= link_to foo_presenter.created_at, foo_path(foo) %> + <% end <% end %> ``` ## Development