Customizing the Behavior of Standard Helpers

Problem

Contributed by: Diego Scataglini

You found a helper that almost does what you need, but you want to alter that helper's default behavior. For example, you would like the content_tag helper to handle a block parameter.

Solution

For this recipe, use an existing Rails application or create an empty one to experiment with. Override the definition of the content_tag helper by adding the following code to app/helpers/application_helper.rb:

def content_tag(name, content, options = nil, &block)
 content = "#{content}#{yield if block_given?}"
 super end

Normally, you would use the content_tag helper like this:

content_tag("h1", 
 @published_bookmark.title + ": " + 
 content_tag("span", 
 "published by " + 
 link_to(@user_login, 
 user_url(:login => @published_bookmark.owner.login),
 :style => "font-weight: bold;"),
 :style => "font-size: .8em;"),
 :style => "padding-bottom: 2ex;")

The previous structure is a bit difficult to follow. Thanks to the modification made to content_tag, you can use blocks to improve the structure of the code:

content_tag("h1", "#{@published_bookmark.title}: ",
 :style => "padding-bottom: 2ex;") do
 content_tag("span", "published by ",
 :style => "font-size: .8em;") do
 link_to(@user_login, user_url(:login => 
 @published_bookmark.owner.login),
 :style => "font-weight: bold;")
 end end

Discussion

In the solution, the value of the block parameter is concatenated to the value of the content parameter. The subsequent call to super delegates all other computation to the original definition of the content_tag helper. When you call super with no parameters, you pass on the arguments in the same order they were received.

The content_tag implementation above is pretty easy to follow. The next example is a little more sophisticated, but the payoff in readability is more than worth the effort required to understand the code. Try replacing your content_tag definition with this:

def content_tag(name, *options, &proc)
 content = options.shift unless options.first.is_a?(Hash)
 content ||= nil
 options = options.shift
 if block_given?
 concat("<#{name}#{tag_options(options.stringify_keys) if options}>",
 proc.binding)
 yield(content)
 concat("</#{name}>", proc.binding)
 elsif content.nil?
 "<#{name}#{tag_options(options.stringify_keys) if options} />"
 else
 super(name, content, options)
 end end

Here's the new content_tag in action:

<%= content_tag "div", :class => "products" do 
 content_tag "ul", :class => "list" do 
 content_tag "li", "item1", :class => "item" 
 content_tag "li", :class => "item" 
 end 
 end 
%>

which generates the following HTML:

<div class="products">
 <ul class="list">
 <li class="item">item1</li>
 <li class="item" />
 </ul>
</div>