Designing Helpers in Ruby on Rails – Introduction
I assume that many of you have written (and still writing) helpers for your views in your awesome Rails projects. A helper, as you all know, is a module which contains methods that are vital for the simplification and the dryness of your views. And because helpers are meant to be used within your views they often contain html code (either plain or ruby code that produces html) which, as you may have noticed, is not very flexible.
A simple example is that you have written an awesome helper method in your ApplicationHelper
that does some magic (but unfortunately contains html) and you want to use the same method in another view but you are bound to use the same html your helper contains (or to avoid that you have to write some ugly code in there!).
Designing Helpers in Ruby on Rails – Detaching
The agile way to follow here is to detach the work of templating a helper method from the method itself. We can achieve this by constructing our little KickAssTemplate
class that will be responsible for templating our helpers (and as you will see not only that!). We create a file lib/kick_ass_template.rb
and a base class
class KickAssTemplate def initialize(object) @object = object end def prepare # Hook method # does all the work of the html generation end def html # Hook method # does the work of html rendering end end
As you have noticed I didn’t write any serious code in there, just a bunch of hook methods and a pretty generic initialize method. You can follow a design pattern here that suits you the best, but for our example we will use the Template Method pattern, kinda fits the name don’t you think? ;).
Designing Helpers in Ruby on Rails – Application
Ok so far so good. Now, to continue, we have to have a real problem. So lets make one! Suppose that you are a developer at stackoverflow and also suppose that stackoverflow is written in ruby :P. A task of yours is that you must create a helper that renders all the recent badges, users awarded with. You also know that this badge module will occur in different places inside the website and with different layouts, a small, a medium and a large in size (time for your KickAssTemplate
). So you first construct a SmallTemplate
class
Note: I will use Nokogiri for the generation of the html (I prefer this approach but you can do it your way).
class SmallTemplate < KickAssTemplate def initialize(label, hash) # the hash will be something like: # {link_to_badge => link_to_user} see stackoverflow.com @label = label #the label for the module e.g. "Recent Badges" super(hash) end def prepare # info: this is a technique for generating # a document with multiple root nodes with Nokogiri @html = Nokogiri::HTML::DocumentFragment.parse "" Nokogiri::HTML::Builder.with(@html) do |doc| doc.div(:class => "smallLabel"){ @label } doc.div(:class => "smallBody"){ doc.ul(:class => "smallList"){ @object.each_pair do |obj1, obj2| doc.li(:class => "smallLink"){ obj1+" "+obj2 } end } } end end def html CGI::unescapeHTML(@html.to_html.html_safe) end end
Similarly constructing classes that handle the medium sized module and the large sized module you finish the first part. The second part is actually constructing the helper. Suppose that you have a Badging
model with a user_id
and a badge_id
column that references a Badge
and a User
. And to get the recent badgings (because that’s what you need) you write (because you are awesome) a scope
class Badgings < ActiveRecord::Base scope :recent, where("created_at < ?", 3.days.ago) end
and in your controller you create the hash of a form that will be handy i.e. {badge_1 => user_1, badge_2 => user_2, …}
class SomeController < ApplicationController def some_action # code in here @badgings = Hash[*Badgings.recent.map{|b| [b.badge, b.user]}.flatten] end end
Thus your helper method should read something like the following
module ApplicationHelper def render_module(*args) options = args.extract_options! # set the hash to be the first argument hash = args.first # choose the template and give one default as well as the label template_class = options[:template]||MediumTemplate label = options[:label] # create the hash of the form {link_to_badge => link_to_user} link_hash = {} hash.each_pair do |k,v| link_hash.merge!({ link_to(k.name, k) => link_to(v.name, v) }) end # render the module template = template_class.new(label, link_hash) template.prepare template.html end end
Finally in your view you call the helper as
<%= render_module @badgings, :label => "Recent Badges", :template => SmallTemplate %>
Designing Helpers in Ruby on Rails – Flexibility
To show you how flexible this design is, suppose that you get assigned to a new task which is to create a new module that uses the same layouts as the Badges module (i.e. small, medium and large) but displays the recent tags together with their count. The changes you have to make are very very little. In fact you only have to change the render_module
helper as
module ApplicationHelper def render_module(*args) options = args.extract_options! # set the hash to be the first argument hash = args.first # choose the template and give one default as well as the label template_class = options[:template]||MediumTemplate label = options[:label] # create the hash of interest link_hash = {} hash.each_pair do |k,v| link_hash.merge!({ link_to(k.name, k) => hash.first.first.is_a?(Badging) ? link_to(v.name, v) : v }) end # render the module template = template_class.new(label, link_hash) template.prepare template.html end end
And as you may have guessed in the view (assuming you have available a @taggings
hash of the form {tag_1 => count_1, …})
<%= render_module @taggings, :label => "Recent Tags", :template => LargeTemplate %>
Good, this help me so much!