Refactoring an ERB Template to Haml
- by Liam McLennan
ERB is the default view templating system used by Ruby on Rails. Haml is an alternative templating system that uses whitespace to represent document structure. The example from the haml website shows the following equivalent markup: Haml ERB #profile
.left.column
#date= print_date
#address= current_user.address
.right.column
#email= current_user.email
#bio= current_user.bio
<div id="profile">
<div class="left column">
<div id="date"><%= print_date %></div>
<div id="address"><%= current_user.address %></div>
</div>
<div class="right column">
<div id="email"><%= current_user.email %></div>
<div id="bio"><%= current_user.bio %></div>
</div>
</div>
I like haml because it is concise and the significant whitespace makes it easy to see the structure at a glance.
This post is about a ruby project but nhaml makes haml available for asp.net MVC also.
The ERB Template
Today I spent some time refactoring an ERB template to Haml. The template is called list.html.erb and its purpose is to render a list of tweets (twitter messages).
<style>
form { float: left; }
</style>
<h1>Tweets</h1>
<table>
<thead><tr><th></th><th>System</th><th>Human</th><th></th></tr></thead>
<% @tweets.each do |tweet| %>
<tr>
<td><%= h(tweet['text']) %></td>
<td><%= h(tweet['system_classification']) %></td>
<td><%= h(tweet['human_classification']) %></td>
<td><form action="/tweet/rate" method="post">
<%= token_tag %>
<input type="submit" value="Positive"/>
<input type="hidden" value="<%= tweet['id']%>" name="id" />
<input type="hidden" value="positive" name="rating" />
</form>
<form action="/tweet/rate" method="post">
<%= token_tag %>
<input type="submit" value="Neutral"/>
<input type="hidden" value="<%= tweet['id']%>" name="id" />
<input type="hidden" value="neutral" name="rating" />
</form>
<form action="/tweet/rate" method="post">
<%= token_tag %>
<input type="submit" value="Negative"/>
<input type="hidden" value="<%= tweet['id']%>" name="id" />
<input type="hidden" value="negative" name="rating" />
</form>
</td>
</tr>
<% end %>
</table>
Haml Template: Take 1
My first step was to convert this page to a Haml template in place. Directly translating the ERB template to Haml resulted in:
list.haml
%style form {float: left;}
%h1 Tweets
%table
%thead
%tr
%th
%th System
%th Human
%th
%tbody
- @tweets.each do |tweet|
%tr
%td= tweet['text']
%td= tweet['system_classification']
%td= tweet['human_classification']
%td
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Positive"/>
<input type="hidden" value="positive" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Neutral"/>
<input type="hidden" value="neutral" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Negative"/>
<input type="hidden" value="negative" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
end
I like this better already but I can go further.
Haml Template: Take 2
The haml documentation says to avoid using iterators so I introduced a partial template (_tweet.haml) as the template to render a single tweet.
_tweet.haml
%tr
%td= tweet['text']
%td= tweet['system_classification']
%td= tweet['human_classification']
%td
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Positive"/>
<input type="hidden" value="positive" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Neutral"/>
<input type="hidden" value="neutral" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
<input type="submit" value="Negative"/>
<input type="hidden" value="negative" name="rating" />
%input{ :type=>"hidden", :value => tweet['id']}
and the list template is simplified to:
list.haml
%style form {float: left;}
%h1 Tweets
%table
%thead
%tr
%th
%th System
%th Human
%th
%tbody
= render(:partial => "tweet", :collection => @tweets)
That is definitely an improvement, but then I noticed that _tweet.haml contains three form tags that are nearly identical.
Haml Template: Take 3
My first attempt, later aborted, was to use a helper to remove the duplication. A much better solution is to use another partial.
_rate_button.haml
%form{ :action=>"/tweet/rate", :method=>"post"}
= token_tag
%input{ :type => "submit", :value => rate_button[:rating].capitalize }
%input{ :type => "hidden", :value => rate_button[:rating], :name => 'rating' }
%input{ :type => "hidden", :value => rate_button[:id], :name => 'id' }
and the tweet template is now simpler:
_tweet.haml
%tr
%td= tweet['text']
%td= tweet['system_classification']
%td= tweet['human_classification']
%td
= render( :partial => 'rate_button', :object => {:rating=>'positive', :id=> tweet['id']})
= render( :partial => 'rate_button', :object => {:rating=>'neutral', :id=> tweet['id']})
= render( :partial => 'rate_button', :object => {:rating=>'negative', :id=> tweet['id']})
list.haml remains unchanged.
Summary
I am extremely happy with the switch. No doubt there are further improvements that I can make, but I feel like what I have now is clean and well factored.