#287 Presenters from Scratch pro
- Download:
- source codeProject Files in Zip (131 KB)
- mp4Full Size H.264 Video (30.2 MB)
- m4vSmaller H.264 Video (16.7 MB)
- webmFull Size VP8 Video (19.1 MB)
- ogvFull Size Theora Video (34.7 MB)
In this episode we’ll create a presenter from scratch. The example application we’ll use to demonstrate this is the one we used in the previous episode on Draper [watch, read]. This application has a user profile page that shows a user’s avatar along with other information that they have entered.
As users don’t have to enter information in every field this page needs to be able to display default values instead. We can see this for the user “MrMystery” who has only supplied his username.
Having to handle default values means that there’s a lot of complex logic in the page’s view code.
<div id="profile"> <%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %> <h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1> <dl> <dt>Username:</dt> <dd><%= @user.username %></dd> <dt>Member Since:</dt> <dd><%= @user.member_since %></dd> <dt>Website:</dt> <dd> <% if @user.url.present? %> <%= link_to @user.url, @user.url %> <% else %> <span class="none">None given</span> <% end %> </dd> <dt>Twitter:</dt> <dd> <% if @user.twitter_name.present? %> <%= link_to @user.twitter_name, "http://twitter.com/#{@user.twitter_name}" %> <% else %> <span class="none">None given</span> <% end %> </dd> <dt>Bio:</dt> <dd> <% if @user.bio.present? %> <%=raw Redcarpet.new(@user.bio, :hard_wrap, :filter_html, :autolink).to_html %> <% else %> <span class="none">None given</span> <% end %> </dd> </dl> </div>
This page contains several if
statements, each of which handles the logic for displaying either the entered value or a default value for a field. As this logic is all view-related it’s a good idea to make use of a presenter class as this can clean up the view code considerably. A presenter is a class that has knowledge of a view and a model and is a great object-orientated way to handle complex view logic. We’ll use one in our application so that we can tidy up the user profile.
Writing Our First Presenter
A presenter is a simple Ruby class. To keep our application’s presenters together we’ll create a new presenters
directory under the application’s app
directory and put our user_presenter.rb
file there. In earlier versions of Rails we’d have to add our new directory to the application’s config.autoload_paths
in the /config/application.rb
file. This isn’t necessary in Rails 3.1 as long as we restart the Rails server. The new directory will be picked up automatically when the server starts.
The presenter needs to know about the model and the view it will be dealing with so we’ll pass these in to its initialize
method and assign them to instance variables.
class UserPresenter def initialize(user, template) @user = user @template = template end end
Now we can begin to extract the view logic into this class, but how should we do this? First we’ll need to instantiate our new UserPresenter
class somewhere. With Draper and other presenter libraries it’s common to do this in a controller action, but we’re not going to take that approach here as it’s arguable the controllers shouldn’t be aware of presenters at all.
Instead of modifying the controller we’ll create a new helper method that will create a presenter instance. We’ll call this method present
. It will take the model that we want to create a presenter for and a block that returns a presenter object. We’ll put all of the view template’s code inside the block so that we have the presenter available to our view.
<% present @user do |user_presenter| %> <div id="profile"> <!-- Rest of view code omitted --> </div> <% end %>
We’ll write the present method in the ApplicationHelper
. As well as a model object we’ll specify an optional class argument so that we can customize the presenter class that’s used. If a class isn’t specified we’ll determine the class based on the object’s class name appended with the word Presenter
, so in this case the name will be UserPresenter
. We then call constantize
on that string to return the class constant.
Now that we have our presenter class we’ll instantiate it by calling klass.new
, passing in our model object
and self
, which will be the template object that has the helper methods that we want to access. If a block has been passed in to the method we’ll yield
the presenter and finally we’ll return it.
module ApplicationHelper def present(object, klass = nil) klass ||= "{object.class}Presenter".constantize presenter = klass.new(object, self) yield presenter if block_given? presenter end end
With our present
method we now have a convenient way to access our presenter for any object from any template and we can begin to move code from the template into the presenter. We’ll start with the code which displays the avatar.
<%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>
We’ll replace this in the with a call to a new avatar
method in our UserPresenter
.
<%= user_presenter.avatar %>
We’ll paste the logic we’ve taken from the view into this new method.
def avatar link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url end
Any helper methods we call in our presenter will need to be called through the template, but instead of putting @template
everywhere we’ll follow Draper’s method of using an h
method and add an h method to our presenter that returns the template. We can then use it with the link_to_if
and image_tag
methods so that they’re called through the template.
The code we’ve copied also calls a helper method that we’ve written: avatar_name
. This is defined in the UsersHelper
.
module UsersHelper def avatar_name(user) if user.avatar_image_name.present? user.avatar_image_name else "default.png" end end end
Whenever we have a helper method that takes the same model object we have in the presenter it’s sensible to move it into the presenter and so we’ll move avatar_name
from the UsersHelper
into our UsersPresenter
and make it private. As we already have the current user in the presenter we can remove its user
argument and call the @user
instance variable instead.
class UserPresenter def initialize(user, template) @user = user @template = template end def avatar h.link_to_if @user.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), @user.url end private def h @template end def avatar_name if @user.avatar_image_name.present? @user.avatar_image_name else "default.png" end end end
If we reload our user profile page now it looks exactly the same as it did before so our presenter is working.
Creating a Base Presenter
Any application that uses presenters will probably have more than one so we’ll want to generalize the behaviour between them. Every presenter we create will have the same initalize
and h
methods so we’ll move these into a base class and have our presenters inherit from it.
We’ll need to change the name of the model that’s passed to initialize
in our new base class to something more generic, say object
. This leaves us with no way to reference our user object directly in the UserPresenter
. We could call @object
, but instead we’ll create a class method in the BasePresenter
called presents
that takes a name. This method will define a method with that name which will return the model held in @object
.
class BasePresenter def initialize(object, template) @object = object @template = template end def self.presents(name) define_method(name) do @object end end def h @template end end
The UserPresenter
and any other presenters we create can now inherit from this new class. If we call presents :user
in the UserPresenter
it will give us a user
method that will return the current user and so we can replace all the calls to @user
with user
.
class UserPresenter < BasePresenter presents :user def avatar h.link_to_if user.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), user.url end private def avatar_name if user.avatar_image_name.present? user.avatar_image_name else "default.png" end end end
Tidying The Rest of The View
Our template is a little cleaner now but there’s still a lot we can do to tidy it up. The steps we’d take to do this are very similar to those we took in the episode on Draper so if you want to see exactly what was done you can watch or read that. We’ll make these changes behind the scenes and just show the end result. With the heavy logic moved into the presenter our template is now a lot cleaner.
<% present @user do |user_presenter| %> <div id="profile"> <%= user_presenter.avatar %> <h1><%= user_presenter.linked_name %></h1> <dl> <dt>Username:</dt> <dd><%= user_presenter.username %></dd> <dt>Member Since:</dt> <dd><%= user_presenter.member_since %></dd> <dt>Website:</dt> <dd><%= user_presenter.website %></dd> <dt>Twitter:</dt> <dd><%= user_presenter.twitter %></dd> <dt>Bio:</dt> <dd><%= user_presenter.bio %></dd> </dl> </div> <% end %>
The presenter’s code is shown below. It’s now made up of several short methods that handle all of the page’s logic for us.
class UserPresenter < BasePresenter presents :user delegate :username, to: :user def avatar site_link image_tag("avatars/#{avatar_name}", class: "avatar") end def linked_name site_link(user.full_name.present? ? user.full_name : user.username) end def member_since user.created_at.strftime("%B %e, %Y") end def website handle_none user.url do h.link_to(user.url, user.url) end end def twitter handle_none user.twitter_name do h.link_to user.twitter_name, "http://twitter.com/#{user.twitter_name}" end end def bio handle_none user.bio do markdown(user.bio) end end private def handle_none(value) if value.present? yield else h.content_tag :span, "None given", class: "none" end end def site_link(content) h.link_to_if(user.url.present?, content, user.url) end def avatar_name if user.avatar_image_name.present? user.avatar_image_name else "default.png" end end end
There are a couple things to note about the changes we’ve made to the presenter. Near the top of the file we use delegate
to delegate the username
method to the User
class. We don’t need to make any changes to the username
that comes from the User
model so we can fetch it directly from there.
The template uses Redcloth
to render the user’s bio. This will be useful to have in other presenters so we’ve created a markdown
method in the BasePresenter
that allows us to easily render Markdown code from any presenter. We can then call this method in the UserPresenter
.
def markdown(text) Recarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe end
The handle_none
method handles the fields like twitter
where we want to display “None given” if the user hasn’t entered any information. In this method we use content_tag
to render the default value in an HTML span. This approach works well for simple pieces of HTML like this, but if we wanted to display a more complex piece of markup it would be worth either creating a partial and rendering that or using a markup language like Markaby.
An alternative to the ‘h’ method
Earlier we defined an h
method to give us access to helper methods. If you don’t want to do this an alternative is to set up method_missing
to delegate every unknown method to the template. This is easy to set up in the BasePresenter
so that it works across all of our presenters.
def method_missing(*args, &block) @template.send(*args, &block) end
Now anything that our presenter doesn’t know about will be sent off to the template and when we call a helper method such as image_tag
, we can call it directly instead of having to call it through the h
method.
Accessing Presenters From Controllers
At some point we might find that we need to access a presenter from the controller layer. For example we might want to use it in the UserController
’s show
action to help us to render some JSON by writing something like this:
def show @user = User.find(params[:id]) present(@user).to_json end
This won’t work as our present
method is a helper method, but we can make a present
method for our controllers in the application’s ApplicationController
. The trick to this method is calling view_content
instead of self
when we instantiate the presenter as this is the template object that renders the view.
class ApplicationController < ActionController::Base protect_from_forgery private def present(object, klass = nil) klass ||= "#{object.class}Presenter".constantize klass.new(view_content, object) end end
Testing
Our application is humming along nicely now with its new clean views but what about testing? How should we test our presenters? We should have high-level integration tests using Capybara (we covered this in episode 275 [watch, read]), but one of the advantages of using presenters is that it makes it easier for us to test view logic at a lower level. We’ll demonstrate this next, first with Test::Unit then with RSpec.
To write our first Test::Unit presenter test we’ll make a new presenters
directory under the /test/unit
directory and add a user_presenter_test.rb
file there. The UserPresenterTest
class that we’ll create in this file needs to inherit from ActionView::TestCase
, instead of the usual ActiveSupport::TestCase
as doing this gives us a way to access the view so that we can pass it to the presenter.
We’ll demonstrate this in a simple test which asserts that the text “None given” is returned from the website
method of a UserPresenter
for a user with no details. First we create a new UserPresenter
, passing it a new User
and also the view template. As our test inherits from ActionView::TestCase
we have access to a view
variable that holds the current view template. We then call the presenter’s website
method and assert that it contains the text “None Given”.
require 'test_helper' class UserPresenterTest < ActionView::TestCase test "says when none given" do presenter = UserPresenter.new(User.new, view) assert_match "None given", presenter.website end end
When we run our test by running rake test it passes.
$ rake test Loaded suite /Users/eifion/.rvm/gems/ruby-1.9.2-p180@global/gems/rake-0.9.2/lib/rake/rake_test_loader Started . Finished in 0.155069 seconds. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Being able to access our presenters from tests means that we can use Test-Driven Development to write our presenters, starting off with a failing presenter test and then modifying the presenter so that the test passes.
To test a presenter with RSpec we first make a presenters
directory under /spec
and add a user_presenter_spec.rb
file there. We’ll write a spec that tests the same thing that our last test did. The key to writing presenter specs is to include ActionView::TestCase::Behavior
. This gives us a view variable containing the template in the same way that inheriting from ActionView::TestCase
did when we were using Test::Unit.
The spec itself will be very similar to the test we wrote. Again we create a new presenter and check that its website
method returns the correct text.
/spec/presenters/user_presenter_spec.rb
require 'spec_helper' describe UserPresenter do include ActionView::TestCase::Behavior it "says when none given" do presenter = UserPresenter.new(User.new, view) presenter.website.should include("None given") end end
When we run rake spec
we should see one passing spec and we do.
$ rake spec /Users/eifion/.rvm/rubies/ruby-1.9.2-p180/bin/ruby -S bundle exec rspec ./spec/presenters/user_presenter_spec.rb . Finished in 0.06957 seconds 1 example, 0 failures
Instead of including the Behavior
module in every presenter spec we can make a change to the config block in the SpecHelper file so that it’s automatically included in every spec. This will include the Behavior module in every file under the spec/helpers directory so there’s no need to add it to our presenter specs.
RSpec.configure do |config| config.include ActionView::TestCase::Behavior, example_group: {file_path: %r{spec/presenters}} # Rest of block omitted. end
There’s a gotcha that we need to be aware of when testing presenters. The view
object that we pass to the presenter can access all helper methods except for those defined in the controller. For example if we have a current_user
method in the controller that we’ve specified as a helper method with helper_method
we won’t be able to call it from the view
object in the test. Instead we’ll need to stub out this method and have it return the values we want. A current_user
method will generally depend on a session or cookie value so stubbing it out is a good idea anyway.
That’s it for this episode on creating presenters from scratch. Now that we’ve shown you what’s involved you should have enough information to decide whether to write your own custom solution or to use a gem such as Draper. Either way, if your applications’ views have complex logic it’s well worth considering using presenters to refactor that logic out and to keep your views tidy.