Brendan Loudermilk (@bloudermilk)
Developer, philosophie
Templates are frequently neglected.
You know how to write "clean" markup.
- Markup repetition
- Logic in templates
Good designers repeat themselves.
Good programmers don't.
Abstract interface components.
Use partials.
Highly repetitive.
Painful to test.
<h3>Your Saved Credit Card</h3>
<dl>
<dt>Number</dt>
<dd>XXXX-XXXX-XXXX-<%= @credit_card.number[-4..-1] %></dd>
<dt>Exp. Date</dt>
<dd>
<%= @credit_card.expiration_month %> / <%= @credit_card.expiration_year %>
</dd>
</dl>
<p>
Thanks for ordering! Your purchase has been billed to your credit card:
<strong>XXXX-XXXX-XXXX-<%= @order.credit_card.number[-4..-1] %></strong>
</p>
...are inevitable.
View Helpers live in app/helpers and provide small snippets of reusable code for views.
module CreditCardHelper
def masked_credit_card_number(number)
"XXXX-XXXX-XXXX-" + number[-4..-1]
end
end
<p>
Thanks for ordering! Your purchase has been billed to your credit card:
<strong><%= masked_credit_card_number(@credit_card.number) %></strong>
</p>
- Big projects end up with tons
- Difficult to organize
- Complex logic isn't well suited for them
- Don't feel right
[Decorators] attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Design Patterns: Elements of Reusable Object-Oriented Software
- Wraps a single object
- Transparent interface
- Forwards methods to original object
In our case:
- Adds presentational logic to models without affecting the model itself
class Decorator
def initialize(component)
@component = component
end
def method_missing(method, *arguments, &block)
if @component.respond_to?(method)
@component.send(method, *arguments, &block)
else
super
end
end
def respond_to_missing?(method, *)
@component.respond_to?(method) || super
end
end
class CreditCardDecorator < Decorator
def masked_number
"XXXX-XXXX-XXXX-" + number[-4..-1]
end
# ... other presentational methods
end
class CreditCardsController < ApplicationController
def show
@credit_card = CreditCardDecorator.new(
current_user.credit_cards.find(params[:id])
)
end
end
<p>
Thanks for ordering! Your purchase has been billed to your credit card:
<strong><%= @credit_card.masked_number %></strong>
</p>
Mmmm, that's nice.
Presentation logic that relates directly to a single instance of a model.
Implementing basic decorators is easy, but Draper adds a few helpful features:
- Access to the view context
- Easily decorate collections
- Pretends to be decorated object (helpful for
form_for
and such) - Easily decorate associations
Unique and/or complex UI behavior will quickly outgrow helpers.
<dl class="story-summary">
<dt>Assigned to</dt>
<dd>
<% if @story.assigned_user == current_user %>
You
<% else %>
<%= @story.assigned_user.name %>
<% end %>
</dd>
<dt>Participants</dt>
<dd><%= @story.participants.reject { |p| p == current_user }.map(&:name).join(", ") %></dd>
</dl>
The essence of a Presentation Model is of a fully self-contained class that represents all the data and behavior of the UI window, but without any of the controls used to render that UI on the screen. A view then simply projects the state of the presentation model onto the glass.
Thanks, Backbone.
class StorySummaryView
def initialize(template, story, current_user)
@template = template
@story = story
@current_user = current_user
end
def assigned_user
if @story.assigned_user == @current_user
"You"
else
@story.assigned_user.name
end
end
def participant_names
participants.map(&:name).join(", ")
end
def to_s
@template.render(partial: "story_summary", object: self)
end
private
def participants
@story.participants.reject { |p| p == @current_user }
end
end
<dl class="story-summary">
<dt>Assigned to</dt>
<dd><%= story_summary.assigned_user %></dd>
<dt>Participants</dt>
<dd><%= story_summary.participant_names %></dd>
</dl>
module StoriesHelper
def story_summary(story)
StorySummaryView.new(self, story, current_user)
end
end
In our calling view:
<%= story_summary(@story) %>
Rails comes with View Objects.
<%= form_for @user do |form| %>
<div class="form-field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="form-field">
<%= form.label :email %>
<%= form.text_field :email %>
</div>
<% end %>
class FancyFormBuilder < ActionView::Helpers::FormBuilder
def fancy_text_field(attribute, options = {})
@template.content_tag(:div, class: "form-field") do
label(attribute) + text_field(attribute, options)
end
end
end
<%= form_for @user, builder: FancyFormBuilder do |form| %>
<%= form.fancy_text_field :name %>
<%= form.fancy_text_field :email %>
<% end %>
- Use i18n
- Find gems to do this work for you (eg. simple_form, table_cloth)
Questions or Comments?