Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 136 lines (94 sloc) 6.418 kb
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
1 Liaison
2 =======
3
4 A Rails presenter class.
5
ffbfe89 @mike-burns Link to the raw docs instead of the versioned files. Less chrome.
authored
6 Experienced devs will be interested in the [NEWS](https://raw.github.com/mike-burns/liaison/master/NEWS), [ChangeLog](https://raw.github.com/mike-burns/liaison/master/ChangeLog), and [reference documentation](http://rubydoc.info/gems/liaison/0.0.2/frames).
323fa01 @mike-burns Link to the NEWS, ChangeLog, and Ruby docs at the top of the docs. Menti...
authored
7
2c918c8 @mike-burns Expand the README with a 'how to use this' section.
authored
8 How to Use Liaison
9 ------------------
10
11 Add this to your `Gemfile`:
12
13 gem 'liaison'
14
15 Then run the bundler installer:
16
17 bundle
18
19 You instantiate `Presenter` classes in your controllers, setting them as instance variables so they can be passed to the views. The `Presenter` class takes the model name as a string (`sign_up`, for example) then a hash of options. Currently supported options are `:fields`, a list of attributes on the presenter (`[:email, :password]`); and `:validator`, a class that knows how to validate the data (`SignUpValidator`).
20
21 An instance of the `Presenter` object is Hash-like: it implements the `Enumerable` module, which means it has an `#each` method among many others; it also has a `#[]` method, which you can use to access values just like with the CGI `params` hash.
22
23 The business logic classes (`SignUp` in the below example) live under `app/models` and are tested as normal, except instead of requiring `spec_helper` they can likely require just `rspec`.
24
25 Validator classes (`SignUpValidator` in the below example) live under `lib` and must either descend from `ActiveModel::Validator` or implement the same interface (`.kind`, `#kind`, `#validate` that takes a record, and a constructor that takes a hash of options). They are also unit tested like normal and can likely get away with just requiring `rspec` instead of `spec_helper`. Sadly, in order to hook into the `ActiveModel::Validations` framework, you must pass the validator class itself instead of an object (`SignUpValidator` vs `SignUpValidator.new`).
26
a657198 @mattgillooly Typo fix.
mattgillooly authored
27 Tutorial and Thought Process
2c918c8 @mike-burns Expand the README with a 'how to use this' section.
authored
28 ----------------------------
29
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
30 A major idea of [the presenter pattern](http://blog.jayfields.com/2007/03/rails-presenter-pattern.html) is to break off the business logic from the view object, letting the view logic be a dumb instance that knows how to get, set, and validate values. The business logic can then query the presenter object for the values as needed.
31
32 Look, here's an example business object:
33
34 class SignUp
35 attr_reader :user
36
37 def initialize(presenter, account_builder = Account)
38 @email = presenter[:email]
39 @password = presenter[:password]
40 @account_name = presenter[:account_name]
41
42 @presenter = presenter
43 @account_builder = account_builder
44 end
45
46 def save
47 if presenter.valid?
48 account = account_builder.new(:name => account_name)
49 @user = account.users.build(:email => email, :password => password)
50 account.save.tap do |succeeded|
51 presenter.add_errors(account.errors) unless succeeded
52 end
53 end
54 end
55
56 protected
57
58 attr_accessor :email, :password, :account_name, :account_builder, :presenter
59 end
60
61 It's just a class, which you can unit test as you please. A presenter object is passed in, then we pull the values out, make sure it's valid, and add errors to it as needed. This class does not deal directly with validations, state, or any of the ActiveModel nonsense.
62
63 Now you need to know how to use a `Presenter` object, so this is what the controller looks like:
64
b7be8ec @mike-burns Indent the controller example in the README as needed.
authored
65 class SignupsController < ApplicationController
66 def new
67 @sign_up = presenter
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
68 end
69
b7be8ec @mike-burns Indent the controller example in the README as needed.
authored
70 def create
71 @sign_up = presenter.with_params(params[:sign_up])
72 db = SignUp.new(@sign_up)
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
73
b7be8ec @mike-burns Indent the controller example in the README as needed.
authored
74 if db.save
75 sign_in_as(db.user)
76 redirect_to root_url
77 else
78 render :new
79 end
80 end
81
82 protected
83
84 def presenter
85 Presenter.new('sign_up',
86 :fields => [:email, :password, :account_name],
7b62c02 @croaky old docs resulted in ArgumentError (wrong number of arguments (0 for 1))
croaky authored
87 :validator => SignUpValidator)
b7be8ec @mike-burns Indent the controller example in the README as needed.
authored
88 end
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
89 end
90
91 In our `new` action we simply set the `@sign_up` i-var to an instance of the `Presenter`. In `create` we use that `Presenter` instance, adding CGI params in. Then we pass that to the `SignUp` class defined above and it's all boring from there.
92
93 The `presenter` method in the above example produces a new `Presenter` instance. This instance has a model name (`sign_up`), fields the form will handle (`email`, `password`, and `account_name`), and a validator (`SignUpValidator`). The validator is any instance of `ActiveModel::Validator`, for example:
94
95 class SignUpValidator < ActiveModel::EachValidator
96 def validate_each(record, attribute, value)
97 record.errors.add(attribute, "can't be blank") if value.blank?
98 end
99 end
100
101 You, the author of the business logic class, are in charge of checking in on these validations and errors. For example, before saving any objects you should check `Presenter#valid?`. And after you've saved something to the database you should add any errors onto the presenter using `Presenter#add_errors`.
102
103 Testing
104 -------
105
106 When writing your unit tests it'll be handy to have a mock presenter around, which is why we package a `MockPresenter` class for you to use. It gives you access to the `#have_errors` and `#have_no_errors` RSpec matchers.
107
108
109 describe SignUp, 'invalid' do
110 let(:params) { { :email => '',
111 :password => 'bar',
112 :account_name => 'baz' } }
113 let(:errors) { { :email => "can't be blank" } }
114 let(:presenter) do
115 MockPresenter.new(:valid => false,
116 :params => params,
117 :errors => errors)
118 end
119 let(:account_builder) { MockAccount.new(:valid => true) }
120
121 subject { SignUp.new(presenter, account_builder) }
122
123 it "does not save the account or user" do
124 subject.save.should be_false
125
126 presenter.should have_errors(errors)
127 end
128 end
129
130 Contact
131 -------
132
ffbfe89 @mike-burns Link to the raw docs instead of the versioned files. Less chrome.
authored
133 Copyright 2011 [Mike Burns](http://mike-burns.com/). Distributed under [the three-clause BSD license](https://raw.github.com/mike-burns/liaison/master/LICENSE).
76418bc @mike-burns The unobtrusive presenter pattern for Rails.
authored
134
323fa01 @mike-burns Link to the NEWS, ChangeLog, and Ruby docs at the top of the docs. Menti...
authored
135 Please [open a pull request on Github](https://github.com/mike-burns/liaison/pulls) as needed. Be sure to update the ChangeLog and, if needed, the NEWS. We follow [the GNU ChangeLog format](http://www.gnu.org/prep/standards/html_node/Change-Logs.html).
Something went wrong with that request. Please try again.