Using custom blocks in your views

11 Jan 2007

Up until this afternoon I’ve been secretly scared of Ruby’s blocks. I understand how to do a basic loop

[1,2,3].each {|i| puts i }

and how to use some of the more complex blocks just fine. I don’t know why I’ve felt so intimidated by Ruby in this regard but I think the trepidation is starting to fade. Today I implemented custom blocks in the views of one of my apps.

The problem

I’ve got this page that lists lots of data for a Athlete’s profile. Each profile manages 70 or so pieces of data and I had them separated out into several partials but today it became clear that they all needed to be displayed on one page (they’re mostly small data). Here’s what the code looked like this morning when I first combined things:

    <label>Forty time:</label> <div class='data' id="contact_forty_time"><%= @athlete_contact.forty_time %></div><br />
    <label>Bench Press (Max):</label> <div class='data' id="contact_bench_press_max"><%= @athlete_contact.bench_press_max %></div><br />
    <label>Squat (Max):</label> <div class='data' id="contact_squat_max"><%= @athlete_contact.squat_max %></div><br />
    <label>Clean (Max):</label> <div class='data' id="contact_clean_max"><%= @athlete_contact.clean_max %></div><br />
    <label>Last Season Stats:</label> <div class='data' id="contact_stats_lastyear"><%= @athlete_contact.stats_lastyear %></div><br />
    <label>Prior Year Stats:</label> <div class='data' id="contact_stats_yearbefore"><%= @athlete_contact.stats_yearbefore %></div><br />
    <label>Qualifier/Predictor:</label> <div class='data' id="contact_predictor"><%= @athlete_contact.predictor %></div><br />
    <label>Credits completed:</label> <div class='data' id="contact_units"><%= @athlete_contact.units %></div><br />
    <label>Current GPA:</label> <div class='data' id="contact_gpa"><%= @athlete_contact.gpa %></div><br />
    <label>JC graduation date:</label> <div class='data' id="contact_jc_graduation"><%= @athlete_contact.jc_graduation %></div><br />

Wow.

Get a load of how much redundancy is there. It’s as if Rails didn’t even exist and I just started programming in straight HTML. How 2006 of me.

The first thing I had to do was extract some of this into a helper. I didn’t want to have to type:

    <label>Forty time:</label> <div class='data' id="contact_forty_time"><%= @athlete_contact.forty_time %></div><br />

When

    <%= data :forty_time, 'Forty time' %>

works just as well - and is WAY CLEANER.

So I added the following to my AthletesHelper file (with some fun ‘cleanup’ of the tags):

    module AthletesHelper
      def data(att, label)
        content_tag(:label, label)+
        content_tag(:div, @athlete_contact.send(att),
                                  :id => "contact_#{att.to_s}",
                                   :class => 'data')
      end
    end

And it generates the same code. Handy. The next addition I wanted was to logically separate the pieces of information into sections. This proved a little less beautiful that I’d hoped:

    <ul class='section'>
      <%= content_tag 'h2', 'Strength' %>
      <li><%= data :forty_time, 'Forty time' %></li>
      <li><%= data :bench_press, 'Bench Press (Max)' %></li>
      <li><%= data :squat_max, 'Squat (Max)' %></li>
      <li><%= data :clean_max, 'Clean (Max)' %></li>
    </ul>

The addition of an unordered list is a real problem. I can easily incorporate the list elements into the data method - that’s not an issue. What I can’t do is use any sort of method that wraps the unordered list around the stuff that’s supposed to go in it. What I need is syntax like form_for or fields_for that allows me to start an element, have several ERB statements <%= %> and then close it. All without having to feel like I’m writing HTML in Notepad.

The Solution

It turns out that the syntax used in fields_for to allow a block embedded in ERB is remarkably simple. I ripped the guts out of form_for and used it’s basic block logic to power my own helper that I called, simply ‘section’. First, here’s the view now that I can use a block:

    <%  section 'Strength' do %>
      <%= data :forty_time, 'Forty time' %>
      <%= data :bench_press, 'Bench Press (Max)' %>
      <%= data :squat_max, 'Squat (Max)' %>
      <%= data :clean_max, 'Clean (Max)' %>
    <%  end %>

Way, way, way cleaner. The best part about this is that it’s actually semantic. I’m specifying that I want to start a block named ‘Strength’ and I’m going to put an arbitrary number of elements into it. Am I going to use an unordered list? Doesn’t matter! I can leave that to the helper and keep a single point of code maintenance.

So let’s take a look at the super-complicated (as Ruby always is) method that allows this. Here’s my updated helper:

    module AthletesHelper
      def section(name, &proc)
        raise ArgumentError, "Missing block" unless block_given?
        concat('<ul class="section">', proc.binding)
        concat(content_tag(:h2, name), proc.binding)
        yield # this is where the stuff in the block get's eval'd and inserted
        concat('</ul>', proc.binding)
      end

      def data(att, label)
        content_tag(:li, content_tag(:label, label)+
                         content_tag(:div, @athlete_contact.send(att),
                                           :id => "contact_#{att.to_s}",
                                           :class => 'data'))
      end
    end

It’s that simple.

You may find the part about concat(‘text’, proc.binding) a little confusing - I certainly did. I won’t try to pretend I completely graps it except to say that when concat is passed proc.binding it executes the code in the environment of the proc. In other words,

    concat('<ul class="section">', proc.binding)

is the same as having typed

~~~~html rhtml

<%= '<ul class="section">' %>

~~~~

  • jc said: I've been doing this for a while, but found it horribly ugly. I've recently found a better way. The only problem is you need to use haml open :ul, {:class => "section"} do open :h2, name yield end if you want to output random text, you just use the "puts" within the block so the .html.haml would be - section 'Strength' do = data :forty_time, 'Forty time' = data :bench_press, 'Bench Press (Max)' = etc...

Please if you found this post helpful or have questions.