Let's say we have a user page with different textual and graphical information. Usually it means there will be a lot of Ruby code with conditional logic inside .erb template. Sure it is not a good thing and will create a significant overhead during future application changes if we want to maintain our application. One of the good solutions is to use presenters. Now we are going to build such presenter from scratch without a help of gems which are quite good (e.g. Draper)
Let's start from the controller:
class UsersController < ApplicationController
def index
@users = User.all
end
def show
@user = User.find params[:id]
end
end
As you can see this is the simple well known controller. It has two actions to show users list and to render personal details for the specific user. The next thing is the show.erb
template:
<% 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 URL:</dt>
<dd><%= user_presenter.website %></dd>
<dt>Twitter Name:</dt>
<dd><%= user_presenter.twitter %></dd>
<dt>Bio:</dt>
<dd><%= user_presenter.bio %></dd>
</dl>
</div>
<% end %>
Wow! That's it? Yep. All the logic and rendering of the user data are now inside the user presenter. What is present
method? This is a helper method located in ApplicationController
in order to be accessible from all views.
def present(object, presenter_class_name = nil)
presenter_class_name ||= "#{object.class}Presenter".constantize
presenter = presenter_class_name.new(object, self)
yield presenter if block_given?
presenter
end
The code is straight forward and speaks for itself but why this method is here? Shouldn't be better to create a presenter in UsersController#show
action? We could do it and it a quite popular approach. 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.
Ok, we understand the code from the above but what about the presenter itself? How does it look? What are the methods and what is their output?
class UserPresenter < BasePresenter
presents :user
delegate :username, to: :user
def avatar
link_to_if user.url.present?, image_tag("avatars/#{avatar_name}", class: "avatar"), user.url
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
link_to user.url, user.url
end
end
def twitter
handle_none user.twitter_name do
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 avatar_name
if user.avatar_image_name.present?
user.avatar_image_name
else
'default.png'
end
end
def handle_none(value)
if value.present?
yield
else
content_tag :span, 'None given', class: 'none'
end
end
def site_link(content)
link_to_if(user.url.present?, content, user.url)
end
end
Do you see BasePresenter
class? It includes methods that are common and should be inherited by all presenters we are going to add. For example presents
method or initializer. Let's see how does it look.
class BasePresenter
class << self
def presents(name)
define_method(name) do
@object
end
end
end
def initialize(object, template)
@object = object
@template = template
end
def markdown(text)
Redcarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe
end
def method_missing(*args, &block)
@template.send(*args, &block)
end
end
First of all its initialize method gets two arguments: an instance of a class we want to "decorate" or to "present" and a template which gives us an access to helper methods such as link_to
. Do you remember presents :user
we have seen earlier in the user presenter? Well now you see how this class method looks and it simply creates a method in run time based on the name of the presented instance which in our case @user. Besides that we have method_missing
method and it is responsible to handle all helper methods like link_to
, content_tag
etc. Basically that is it.
The one thing although that might be important. If we want to use our presenter not only from views but in controller as well we need to add something to Applicationcontroller
.
class ApplicationController < ActionController::Base
# ...
protected
def present(object, presenter_class_name = nil)
presenter_class_name ||= "#{object.class}Presenter".constantize
presenter_class_name.new(view_context, object)
end
# ...