Sunday, November 30, 2008

Sharing a partial for both server-side and client-side HTML generation.

Say I have a page that shows a table of items (like a shopping cart), and I want to be able to generate the HTML for this table from the server-side (of course). Let's also say that I want to have client-side javascript be able to dynamically add items to this table without a round-trip to the server, based on data gathered on the client. What I end up with is some HTML code (actually, ERb or Haml) that defines the layout of a row in this table, and a duplicate piece of code in javascript that generates identical HTML. The server-side code uses a nice templating language like Haml to fill in the variable pieces, while the client-side code pieces together HTML embedded in strings concatenated with the dynamic data.

Not very DRY. Not easy to maintain. Not pretty to look at.

So...What's a good solution that allows me to write the HTML once, and have one place to maintain it, while still being able to use it to dynamically evaluate the template based on parameterized data on both the server and client sides?

Below is a solution that I came up with. (And I would love to hear any better ideas).

For brevity, I'll use a very simple HTML structure in these examples, but in practice I have used this with much more complex structure and logic. Here's what our example table looks like:

<table id="items">
<td class="name">Shirt</td>
<td class="desc">White short-sleeved shirt</td>
<td class="price">$5.99</td>

<td class="name">Pants</td>
<td class="desc">Blue pants with zipper</td>
<td class="price">$25.99</td>

Assume we have this built using Haml, and there are two partials: one for the top-level table, and one for an individual row. E.g.:


- items.each do |item|
= render :partial => 'item_row', :locals => {:item => item}


%td.desc= item.desc
%td.price= item.price

In the client-side javascript in our top-level Haml partial, assume we have some function that gets called from some user input action (like filling out a form and clicking a button). E.g.:

var addItem = function(name, desc, price) {
var html =
"<tr>" +
" <td class=\"name\">" + name + "</td>" +
" <td class=\"desc\">" + desc + "</td>" +
" <td class=\"price\">" + price + "</td>" +
$('items').insert(html); // using prototype.js
}; this doesn't seem so bad with such a simple example, but imagine that the HTML structure for a table row is something much more complex, with conditionals based on the item, and maybe even usage of additional sub-partials. This maintenance for this becomes a nightmare.

This first thing I am going to do is change the item_rows.haml partial. Instead of having it actually render the item's values, it's going to render template replacement variables using #{...} syntax. Note, these are NOT interpolated in the partial. We'll do that in a separate phase from rendering:


%tr #{item_name}
%td.desc #{item_desc}
%td.price #{item_price}

Next, let's make a couple helper methods. The first one returns this partial rendered as a string. (I do this because in the real world, this takes a number of parameters, and adding a helper layer makes things easier to manage than always calling render directly). The second one renders this partial and then interpolates the #{...} strings inside it, based on the properties of the item we want to render -- effectively making this equivalent to rendering the original version of this template.


module ItemHelper
# Make ActionController::Base#render_to_string available.
helper_method :render_to_string

def item_row_template
render_to_string :partial => item_row

def item_row(item)
# Get the template.
t = item_row_template

# Define the template replacement variables.
item_name =
item_desc = item.desc
item_price = item.price

# Evaluate the template
eval(t.inspect.gsub("\\#", "#"))

This, of course, means we change the top-level partial to use this new item_row helper, like so:


- items.each do |item|
= item_row(item)

At this point, we've got a functionally equivalent server-side. But the big benefit is that now the HTML template returned by the item_row_template helper method can also be used to do the same substitutions on the client side, thanks to the handy Template class in prototype.js. The Haml that defines the javascript addItem method now looks like this:

var addItem = function(name, desc, price) {
var html = new Template(
item_name : name,
item_desc : desc,
item_price : price

Now, when the markup for the item row changes, it only needs to change in one place, and both the server-side and client-side benefit from it. This certainly feels more DRY...but also seems like a hefty price to pay in terms of complexity...

No comments: