Haml / Grigsby's First Rule

Posted by Dan Grigsby
on Monday, April 09

“Any sufficiently complicated rhtml partial contains an ad hoc, informally-specified, bug-ridden, implementation of half of Haml.”

With apologies to Philip Greenspun, I’m claiming the above quote as “Grigsby’s First Rule.”

I’ve never had a rule before, and I’m fairly certain that someone else is supposed to name a rule after you, but as this is my inaugural Rail Spike post I don’t have a cadre of readers to bestow the honorific, so I’m declaring it myself after I spent half a week fighting with a repeating pattern I’d noticed in my views:

I use a variant of this HTML fragment throughout my application:

1
2
3
4
5
6
7
8
9
<div class="MODEL_NAME">
  <div class="ATTRIBUTE_NAME_1">
    ATTRIBUTE_VALUE_1
  </div>
  ...
  <div class="ATTRIBUTE_NAME_N">
    ATTRIBUTE_VALUE_N
  </div>
</div>

This little fragment shows up, slightly modified, all over the place. There’s one for my User class, one for my Order class; there’s one for each class. Here’s the version for my Offering class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="offering">
  <div class="name">
    Foo
  </div>
  <div class="symbol">
    foo
  </div>
  <div class="last_price">
    $100    
  </div>
  <div class="last_traded_at">
    1 Apr 21:21
  </div>
  <div class="actions">
    <a href="/offering/foo/bid/new">buy</a>
  </div>
</div>

From class to class, the structure is the same, but the attributes vary, both by name (e.g., email is unique to User) and format (e.g., some are links, some are formatted numbers).

Initially, I had one partial that output this structure for each model.

Using one of these partials with a collection, I could spit out a list of all of the instances of a model that, when paired with the appropriate CSS, shows in a grid view. The same partial can show a single instance and can be “dressed up” with other details and presented in a vertically rendered, two column, name/value view.

Put in RESTFul terms, each model has a controller that uses its partial with a collection for GET/index and with a single instance in GET/show.

Re-inventing Haml

Looking at these partials, and seeing their similarity, I decided to try and generalize their form into a partial I could use for any arbitrary model object (or collection of model objects).

This would have been easy if I’d wanted to display all of the attributes every time and without formatting. Instead, I needed to show some of the attributes and apply different formatting to each.

My first step was to pass an array to the partial (along, of course, with the model itself) containing the symbols of the attributes to display.

Next, I modified the partial so that the array elements could optionally be a hash containing the name and format of the output.

This worked, but setting up the partial to produce the output was actually worse than repeating the same un-DRY ERB. I’m embarrassed to show it but, for posterity’s sake, this example proves the point:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%= 
  attributes = []
  attributes << :name
  attributes << :symbol
  attributes << { :attribute => :last_price; :eval => number_to_currency(offering.last_price) }
  attributes << :last_trade_at

  actions = <<-EOT
    link_to "buy", offering_new_bid_path(offering) 
    link_to "sell", offering_new_ask_path(offering) if offering.owners.find_by_user_id(session[:uid])
    '&nbsp;'
  EOT
  
  attributes << { :attribute => :actions, :eval => actions }

  render :partial => "shared/container", :object => @offerings, :locals => { :attributes => attributes }
%>

Looking at that code jammed into a view made me queazy. I’d clumsily created a domain specific language for creating structured DIVs, a problem I vaguely suspected that I’d seen solved before in the form of Haml.

Haml

I didn’t really get Haml when I first was introduced to it. Haml is usually described as a replacement for ERB. Although I’ve never seen it described this way, I think of Haml as a DSL for creating structured X/HTML. I think this makes it more palatable to folks accustomed to ERB and, given that you can freely intermingle Haml partials with ERB templates, this is a good way to introduce it.

Taking a cue from Hampton Catlin, Haml’s creator, I’m going work backwards into Haml from some ERB. Using the well-formed html structure and atrocious example above as inspiration, here’s ERB to produce the HTML I want:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="offering">
  <div class="name">
    <%= offering.name %>
  </div>
  <div class="symbol">
    <%= offering.symbol %>
  </div>
  <div class="last_price">
    <%= number_to_currency offering.last_price %>    
  </div>
  <div class="last_traded_at">
    <%= last_trade_at.to_formatted_s(:short) %>
  </div>
  <div class="actions">
    <%= link_to "buy", offering_new_bid_path(offering) %>
    <% if offering.owners.find_by_user_id(session[:uid]) %>
      <%= link_to "sell", offering_new_ask_path(offering) %>
    <% end %>
  </div>
</div>

Hampton says he came up with Haml by starting with a fragment like this and seeing what he could take out without loosing anything. I’ll follow his example.

Closing tags and blocks

First, let’s clip out the places where HTML tags and Ruby blocks are closed. We can do this because our code’s indented properly; I’m not religious about code alignment, but with HTML it’s a habit I’ve developer after too many hours wasted trying to figure out /which/ tag I forgot to close. The result:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="offering">
  <div class="name">
    <%= offering.name %>
  <div class="symbol">
    <%= offering.symbol %>
  <div class="last_price">
    <%= number_to_currency offering.last_price %>    
  <div class="last_traded_at">
    <%= last_trade_at.to_formatted_s(:short) %>
  <div class="actions">
    <%= link_to "buy", offering_new_bid_path(offering) %>
    <% if offering.owners.find_by_user_id(session[:uid]) %>
      <%= link_to "sell", offering_new_ask_path(offering) %>

I don’t need to close the divs (e.g., no </div>) and blocks, because they can be inferred from the indents. Already this tickles my information density fancy. Ruby’s expressive; placing a lot of functionality in s small space (without resorting to golfing) is one of it’s beauties. This first step in hamilization (is that a word?) already seems like a step in the right direction.

< > Brackets

Everything that’s left is bracketed in less than and greater than characters. We can remove them and everything will still be recognizable; while I’m at it, applying the same logic, I’m going to remove the % character from the opening and closing of the ERB tags:

1
2
3
4
5
6
7
8
9
10
11
12
13
div class="offering"
  div class="name"
    = offering.name
  div class="symbol"
    = offering.symbol
  div class="last_price"
    = number_to_currency offering.last_price
  div class="last_traded_at"
    = last_trade_at.to_formatted_s(:short)
  div class="actions"
    = link_to "buy", offering_new_bid_path(offering)
    - if offering.owners.find_by_user_id(session[:uid])
      = link_to "sell", offering_new_ask_path(offering)

(I slipped one extra character in for the “if” line; its ERB produced no output and I needed a way to tell Haml to execute the ruby without output.)

Aesthetically, I think this last edit is a step in the wrong direction, but with patience it’ll resolve itself shortly. This isn’t yet Haml; it’s a proto-Haml I suppose, and while not yet beautiful it’s understandable and mostly parse-able.

Shortcuts, from CSS

We’re still repeating ourselves in a lot. Every div has a class attribute. In a stylsheet, the ”.” character represents a class, so I’ll substitute that here:

1
2
3
4
5
6
7
8
9
10
11
12
13
div.offering
  div.name
    = offering.name
  div.symbol
    = offering.symbol
  div.last_price
    = number_to_currency offering.last_price
  div.last_traded_at
    = last_trade_at.to_formatted_s(:short)
  div.actions
    = link_to "buy", offering_new_bid_path(offering)
    - if offering.owners.find_by_user_id(session[:uid])
      = link_to "sell", offering_new_ask_path(offering)

(Though there aren’t any in the above example Haml, like CSS, uses the ”#” character to indicate id.)

Designers love the fact that their CSS kung fu works inside Haml. For this reason, it’s easier to sell Haml to designers than it is to devs.

Last step; produces usable Haml

At this point, we’ve reduced our template to div shortcuts and ruby blocks. Divs are, of course, the most common tag in well structured X/HTML. In Rails, we default to the most-common-case when something is left unspecified by “convention.” Haml shares Rails’ philosophy in this matter:

In Haml, unless specified, a tag is assumed to be a div. (To specify a tag other than a div, preface the tag name with a %, e.g, “%h2 This is an h2 tag”.) Applying this convention, we get:

1
2
3
4
5
6
7
8
9
10
11
12
13
.offering
  .name
    = offering.name
  .symbol
    = offering.symbol
  .last_price
    = number_to_currency offering.last_price
  .last_traded_at
    = last_trade_at.to_formatted_s(:short)
  .actions
    = link_to "buy", offering_new_bid_path(offering)
    - if offering.owners.find_by_user_id(session[:uid])
      = link_to "sell", offering_new_ask_path(offering)

That’s syntactically correct Haml.

The 20^h minute test drive

The Hamltonians entreat you to spend twenty minutes converting one of your existing templates with the promise that you’ll never go back to ERB. This is probably true, but I think it’s more compelling to whip up a template for a model from scratch. Fire up the editor of your choice and:

Start by writing the name of the model on a line by itself, prefaced with a period (to indicate the class name will be the name of the object).

Next, pick which model attributes you want to display and enter them, one per line, indented two spaces, also prefaced with a period (to indicate that the class name will be the name of the attribute).

That’s all it takes to create the structure of the X/HTML. Now go back and and using the same Ruby you’d use for ERB to display values, links, whatever.

That’s it; all done; bet it took about two minutes.

I’ll break here for now. Check back; I’ve got follow up article cooking to discuss some Haml tricks, some instances where Haml isn’t well suited, and some other CSS related goodies.

Comments

Leave a response

  1. Barry HessApril 09, 2007 @ 09:01 PM

    Soon I plan to start with one of these DSL’s, if that’s the right term. Wondering if you’ve looked at Markaby? I am leaning toward Markaby because it uses Ruby and is unconcerned with whitespace. I can see the advantages of Haml and its brevity, though.

  2. Dan GrigsbyApril 10, 2007 @ 12:32 AM

    Barry: Yep, I’ve looked at Markaby.

    I prefer Haml primarily because I think its use of a div as a reasonable default, as well as sharing with CSS the same syntax for setting the class and/or id, make it faster to produce structured xhtml.

    If was I doing old-school html with lots of table and the like I might have chosen Markaby.

    Also, there are some nice side-effects of using haml, primary amongst them is that it produces beautifuly laid out X/HTML. This makes debugging rendering issues a ton easier.

    Finally, there’s a momentum thing. Compare these two charts:

    http://technorati.com/chart/haml http://technorati.com/chart/markaby

    Haml’s showing up all over the place. IIRC, the trunk version of Typo is switching its default templates to Haml. There’s a little “judged by the company you keep” aspect of this that I think is worth considering.

  3. Mr eel April 10, 2007 @ 03:51 AM

    I’m not usually one to bug about markup used in examples since I understand it’s usually just for illustrative purposes and may be contrived to make a good example.

    But that’s some seriously nasty code. Wrapping everything in DIV tags? Yuck! In the particular example above it makes more sense to use a UL or perhaps even a DL if you wanted to label every attribute.

    DIV tag soup looks as old as a table based layout in my opinion.

    By the way, thank you for writing this. It was the introduction to HAML I didn’t know I needed! I’ll have to investigate it more closely now — after initially passing over it.

  4. Barry HessApril 10, 2007 @ 10:04 PM

    Dan – Thanks for the background on your choice. Very convincing. I’ve noticed chatter here and there that Markaby doesn’t have a lot of steam … looks like there’s some truth to that.

    Haml, here I come.