Very often, when writing views in Rails and the like, we come across chunks of HTML which are repeated in various places, but with slight variations, and we are stuck between choosing whether to bother DRYing up the code by using helpers, or just to have partly duplicated markup dotted around the place.
Let me give an example.
Supposing we have a navigation menu that looks like this:
<div class="nav">
<h2 class="super_dooper_header">Teams</h2>
<p>Follow the links to your team!</p>
<ul>
<li class="tottenham"><a href="/tottenham">Tottenham</a></li>
<li class="liverpool"><a href="/liverpool">Liverpool</a></li>
<li class="chelsea"><a href="/chelsea">Chelsea</a></li>
</ul>
</div>
And on another page we have one that looks like this:
<div class="nav">
<h2 class="super_dooper_header">London Teams</h2>
<ul>
<li class="tottenham">
<a href="/tottenham"><span class="icon">Tottenham</span></a>
</li>
<li class="chelsea">
<a href="/chelsea"><span class="icon">Chelsea</span></a>
</li>
</ul>
</div>
There is some shared markup which would be nice to DRY up, but maybe not all of it. So do we use helpers/partials? Or do we leave it as it is?
Solution 1
Leave it as it is - no partials or helpers (obviously we might use the standard helpers like link_to etc.).
The problem with this is that it isn't DRY. Supposing we have ten of these menus. And supposing we suddenly need to change the <h2 class="super_dooper_header"> to a <h3 class="not_so_super_header">.
We have to change it in a ten places. Yuk!
Solution 2
Use a partial/helper, and pass in optional arguments.
The problem with this is that if our markup is fairly complex, we can soon find ourselves passing in a million configurable options that become ridiculously complicated and obscure the meaning:
<%= nav_menu :header => 'Teams',
:help_message => true,
:teams => ['Tottenham', 'Liverpool', 'Chelsea'] %>
and
<%= nav_menu :header => 'London Teams',
:help_message => false,
:teams => ['Tottenham', 'Chelsea'],
:icon_links => true %>
And what if one of the links changed? If href="/tottenham" changed to "/spurs", we'd need to pass in something like
:teams => {'Tottenham' => 'spurs',
'Liverpool' => 'liverpool',
'Chelsea' => 'chelsea'}
YUK!!!
Solution 3
Use smaller helpers, e.g.
<div class="nav">
<%= nav_header 'Teams' %>
<p>Follow the links to your team!</p>
<ul>
<%= nav_item 'Tottenham' %>
<%= nav_item 'Liverpool' %>
<%= nav_item 'Chelsea' %>
</ul>
</div>
<div class="nav">
<%= nav_header 'London Teams' %>
<ul>
<%= nav_item 'Tottenham', :icon => true %>
<%= nav_item 'Chelsea', :icon => true %>
</ul>
</div>
Hmmm. This is better, but it's still not as DRY as it could be. If we decided to add icons to the first menu, we'd have to modify each nav_item with :icon => true or similar, which in the example is three times, but for a long list could be much more.
Is there a better way?
Of course there is! Otherwise I wouldn't be writing this blog post!!
Solution 4 - Block Helpers
Rails already uses this pattern in its form builders.
We create a block helper which yields a renderer-type object. In this way, we can DRY up as much or as little as we want:
<% nav_menu do |m| %>
<%= m.header 'Teams' %>
<p>Follow the links to your team!</p>
<ul>
<%= m.item 'Tottenham' %>
<%= m.item 'Liverpool' %>
<%= m.item 'Chelsea' %>
</ul>
<% end %>
and:
<% nav_menu :icons => true do |m| %>
<%= m.header 'London Teams' %>
<ul>
<%= m.item 'Tottenham' %>
<%= m.item 'Chelsea' %>
</ul>
<% end %>
Much nicer!
So how do you create one of these block helpers?
You can create your own like this by creating a new class (for the yielded renderer) and playing around with actionview's concat and capture methods (left as an exercise for the reader!)
However, the easiest way is to use the block helpers gem on github.
Using that gem, your helper file would look something like this:
module ApplicationHelper
class NavMenu < BlockHelpers::Base
def initialize(opts={})
@icons = opts[:icons]
end
def header(text)
%{<h2 class="super_dooper_header">#{text}</h2>}
end
def item(text)
inner_html = @icons ? content_tag(:span, text, :class => 'icons') : text
content_tag :li, link_to(inner_html, "/#{text.downcase}"), :class => text.downcase
end
def display(body)
content_tag :div, body, :class => 'nav'
end
end
end
As you can see, this is just like writing ordinary helpers, only you're writing them in a block helper class.
Note the following:
- the name of the block helper class is the camelized version of the block helper method that you call in the view, which is automatically defined for you (NavMenu => nav_menu)
- any arguments you pass to the block helper are passed to the initialize method. In this way, you can make use of instance variables in your helpers
- if you define the method display (which takes the body of the block as an argument), you can do things like surround the block with markup (like in the example above), post-process the block, etc.
For more information see the README.
