Pretty Blocks in Rails Views

One of the easiest ways to improve the readability and reusability of a Rails application is to refactor the view layer. I find that most Rails code I look at in models and controllers tends to be very good, but views are a huge mess of single-use partials, repeated behavior, and lots and lots of missed opportunities to implement some really graceful and reusable helpers. I wanted to whomp up a quick blog post discussing some of my favorite techniques for making views prettier.

Use Blocks in Helpers

Ruby makes extensive use of blocks. So as good Rails programmers we want to follow in that grand tradition and make everything that can be blocky blocked. One of the first pieces of code I add to the application helper for a project is an override to link_to to accept a block:

def link_to(*args,&block)
  if block_given?
    concat(super(capture(&block), *args), block.binding)
  else
    super(*args)
  end
end

This functionality is already in Edge Rails, and if you actually use this helper there it will fail because of it. This makes it extremely easy to make links of all kinds in your application:

<% link_to(users_path) do %>
  <%= image_tag "user_button.png" %>
  Click here to add a user
<% end %>

Imagine how ugly that would be without the use of a block! It would be one big multi-line mess. Here, we have clarity and ease of use, and we don't sacrifice anything. In this example I use the two most important methods for making views accept blocks: concat and capture. Through a more common use case, let's take a closer look at these methods, and why block helpers can make your life a whole lot easier.

Rounded Corners

We want to add some rounded corners to our application. Without any code, this is how we make them:

Rounded corners are so Web 2.0.

Initially it might be tempting to make two methods to output the start and end of the helper:

<%= rounded_top %>
Rounded corners are so Web 2.0.
<%= rounded_bottom %>

These are easy helper methods that we can clearly understand. Here's what the first looks like.

def rounded_top
  return '<div class="rounded_box">\n' +
    '<div class="rounded_corner_top"></div>\n' +
    '<div class="rounded_corner_content">'
end

But now we have a fair amount of ugly HTML in our helper. And will there ever be a case that we call rounded_top without wanting rounded_bottom an arbitrary amount of time afterwards? The answer would be no, so let's link the two together in a much more clever manner.

<% rounded_block do %>
Rounded corners are so Web 2.0.
<% end %>

This is an ERb evaluation block, not an output block: this is because we are going to replace the content directly on the page rather than output anything at all. (Thus our helper method will function similarly to other Rails evaluation blocks like form_for.) So let's make it happen.

def rounded_block(&block)
  concat(content_tag(:div, :class => "rounded_box") do
    content_tag(:div, " ", :class => "rounded_corner_top") +
    content_tag(:div, :class => "rounded_corner_content") do
      capture(&block)
    end +
    content_tag(:div, " ", :class => "rounded_corner_bottom")
  end, block.binding)
end

Let's dissect this helper quickly. Rounded_block accepts one argument, a block: in the middle, we capture that block, removing it from the page and returning it inside the helper. (We call this instead of yield, because if we yielded the block, it would appear twice on the page.) We add three content divs together and finally concat the whole shebang onto the block's binding which causes it to actually be displayed in the layout. Remember the plusses! Without them you'll only get the last content_tag, which is almost certainly not what you want.

This method is highly extensible and extremely flexible. Here's an example of the rounded_corner code I actually use in my applications:

<% rounded_yellow_box(:background => true, :title => "What I think about rounded corners", :id => "rounded_thoughts" ) %>
  They're so Web 2.0-y.
<% end %>

And the code for it:

def rounded_box(color, options = {}, &block)
  raise ArgumentError, "Missing block" unless block_given?
  options.symbolize_keys!
    concat(content_tag(:div, :id => options[:id], :class => "rounded_box") do
      if options[:title] then content_tag(:div, options[:title], :class => "rounded_title") else "" end +
      content_tag(:div, " ", :class => "rounded_corner_top") +
      content_tag(:div, :class => if options[:background] "rounded_corner_content" else "rounded_corner_no_bg" end) do
        capture(&block)
      end +
      content_tag(:div, " ", :class => "rounded_corner_bottom")
    end, block.binding)
end

def method_missing(method, *args, &block)
  if method.to_s =~ /^rounded_([a-z]+)_box$/
    rounded_box($1, *args, &block)
  else
    super
  end
end

A little dense, but extremely flexible: you can use helper methods like this to generate a lot of content situationally and change your block elements around quite a bit. Having this sort of agility can be invaluable in projects that demand many similar but different parts of the view layer.

Extending Blocks to Partials

Let's be honest: that rounded_block helper, though attractively simple, uses a fair amount of content_tags. Can we make this even simpler? Turns out we can by employing a little-used feature of partials: layout.

show.html.erb
<% render(:layout => 'rounded') do %>
  Rounded corners are so Web 2.0.
<% end %>

_rounded.html.erb:
<div class="rounded_box">
  <div class="rounded_corner_top"> </div>
    <div class="rounded_corner_content"><%= yield %></div>
  <div class="rounded_corner_bottom"> </div>
</div>

Or to really simplify your life, you can do the partial render on one line, if you're trying to put one partial into our rounded_corner layout:

_content.html.erb
Rounded corners are so Web 2.0.

show.html.erb
<%= render :partial => "content", :layout => "rounded" %>

Couldn't be easier. You can even pass locals and collections to the partial call just as you might to a regular call to render partial:

<% render(:layout => 'rounded', :locals => {:title => "What I think about rounded corners", :id => "rounded_thoughts", :background => true}) do %>
  Rounded corners are so Web 2.0.
<% end %>

And then in _rounded.html.erb:

<div class="rounded_box">
  <div class="rounded_corner_top" id="<%= id %>"> </div>
    <% if title %>
      <div class="rounded_title"><%= title %></div>
    <% end %>
    <div class="<%= background ? "rounded_corner_content" : "rounded_corner_no_bg" %>"><%= yield %></div>
  <div class="rounded_corner_bottom"> </div>
</div>

To Sum It All Up

So which way is better? Both solutions offer a lot to DRY up your application with: they make your code extremely reusable and powerful. Theoretically speaking the method_missing happiness I inserted earlier is slow, but the pages will probably be cached at this point anyway so really it's just a matter of style. I tend to use helper methods when things seem extremely programmatic to me: passing an options hash is very Rails-y and sensible. Conversely, I use partials when the layout is almost entirely HTML that won't be changing very much. But really the two methods are very interchangeable: just make sure to use them when you've got a lot of repeating code!

And as a small end-note: Edge Rails is changing the way that concat and capture work. concat will no longer need a binding to be attached to the page. Now it binds directly to the new @erbout instance variable and is appended straight to the buffer, which not only improves performance, but increases the readability of the helpers I made above.

Related Services: Ruby on Rails Development, Custom Software Development