Dynamic page layout with javascript: cloneCopy and templates

So I’m building a web application that uses javascript (with the canvas element) to draw a chart of student grades over a period of time. It also lists each grade in a table on the side of the chart. Since the grade data is dynamic (coming in the form of JSON either at page load or asynchronously upon update), I have to draw the table programatically. For a while, I did this in a really nasty way, then I discovered the Prototype.js Template class and javascript’s own Element.cloneCopy function. And then it started to get all better.

(Update: It turns out IE wasn’t as amenable as I thought, so I’ve updated the code for working with the cloned table node below.)

Putting HTML elements into a webpage dynamically can be accomplished in a few ways. One is through the DOM, creating elements, assigning characteristics and innerHTML to them, and then inserting them into a tree of elements that you finally insert into the page’s existing node tree. It’s wordy and complicated, and it’s pretty easy to look at your DOM-manipulation code and have no idea what it’s accomplishing. Here’s a hideous example:

tbody = document.createElement("tbody");
table.appendChild(tbody);
row = tbody.insertRow(-1);
Element.addClassName(row, "average");
label_cell = row.insertCell(-1); Element.writeAttribute(label_cell, "colspan", "2");
label_cell.innerHTML = "average"; Element.addClassName(label_cell, "summary");
value_cell = row.insertCell(-1); Element.writeAttribute(value_cell, "colspan", "2");
value_cell.innerHTML = displayScore(average);

Quick–what HTML does this produce? It generates this (some formatting added for readabiity), although you’d never know:

<tbody><tr class="average">
<td colspan="2" class="summary">average</td>
<td colspan="2">94</td>
</tr></tbody>

The other way to code up elements is with javascript that outputs HTML as text. When the text is ready, you assign it to some existing page element’s innerHTML attribute. This way, your code does look more like the desired output:

html = '<tbody><tr class="average">' +
'<td colspan="2" class="summary">average</td>' +
'<td colspan="2">' + displayScore(average) + '</td>' +
'</tr></tbody>'

There are two problems with this. First, if your output gets more complicated, the ‘string’ + ‘string’ approach can get nasty and confusing. What if you have many pieces of data to put into the HTML? That’s a lot of single quotes and plus signs to keep count of. And if you need to have a single quote in your content? Then you have to remember to backslash-escape them.

The bigger problem comes courtesy of Internet Explorer, the reliable source of lots of pain for web developers. In IE, you *can’t* write a table out with the innerHTML method. You actually have to use DOM methods like insertRow and insertCell to build your table elements. Ugh.

A Better Way

What would have been a lot better than either of these solutions, actually, would be write out the whole table in HTML and have it in the source to start with. That way I could do some old-fashioned HTML coding and viewing to make sure everything laid out correctly. I could put placeholder values into the cells to help simulate actual data layout. Then I would use a templating system to switch the placeholders out for actual data. I would need to repeat this process when asynchronous updates were made, so it would be good to have the table in a hidden div and then just copy it, run the templating on the copy, and insert the new table into the place on the page where it was to be displayed.

Well, it turns out it’s pretty easy to implement this “a lot better” solution. First, the prototype.js library offers a nice Template class. I can indeed write out my table’s HTML, including this element for the average row:

<tbody>
<tr class="average">
<td colspan="2" class="summary">average</td>
<td colspan="2" id="average">#{average}</td>
</tr>
</tbody>

The Ruby-like #{} denotes a placeholder that the Template class will recognize. Now I just have to make sure that I have a hash with keys that match my placeholder names and values that are ready to be inserted.


data = {'name': "Algebra",
'average': "90",
'goal': "A"};

In this example, there are other data points besides average. I’ve given each cell an id that’s the same as the placeholder name (and the corresponding data hash key). That way, I can use some more of prototype’s methods to do this:

$w("name average goal").each(function(symbol) {
var tmpl = new Template($(symbol).innerHTML);
$(symbol).innerHTML = tmpl.evaluate(data);
});

And that does it. I can update the whole table with dynamic data. But I want to keep the table around and work on a copy of it. And I still have to make my copy appear when it’s ready.

Here’s where cloneCopy comes in. I put my reference table, with the placeholder, in a DIV element that’s styled to be hidden. Now, I can get an entire node-tree copy of the table like this:

var new_table_node = $("placeholder_table").cloneNode(true);
new_table_node.id = "cloned_table";

The true argument to cloneCopy tells the function that I want a complete copy of the node tree, not just the top-level table element. I immediately give my clone a new id attribute, since I’m going to insert it into the HTML document, and it will need a unique id.

Now I can perform my templating work on the new table node. I need to make an edit to my code, since I’m now updating not the original reference table but my new copy. So the selection of placeholder-bearing cells needs to look for not just the cells’ ids, but the right enclosing table:

$w("name average goal").each(function(symbol) {
var target = new_table_node.select("#"+symbol)[0];
var tmpl = new Template(target.innerHTML);
target.innerHTML = tmpl.evaluate(data);
});

OK, so I shouldn’t use id attributes in the table cells’ HTML but rather CSS classes. That way keep things clean by avoiding duplication of element ids. But I’ll leave that as an exercise for the reader. (And myself.)

Now I’m ready to go, so I just insert my new_table_node into the table with this prototype.js function that wraps built-in DOM insertion methods:

$("grades_table").insert(new_table_node);

And that’s it. It works just fine in IE, too. Actually it doesn’t quite work in IE. And by not quite I mean a cryptic error message pops up and the table doesn’t appear. Oh, Internet Explorer. Someday you’ll grow up to be a real browser.

The problem with IE is it won’t let you perform certain DOM operations on the cloned node unless the node is inserted into the page. So, let’s bite the bullet and put the cloned table in place before we run templating and DOM-manipulation code on it. We will make one change, though, to add the “hidden” CSS class to the node so that it won’t appear on the page when it’s appended into place. (This assumes I’ve defined a “hidden” CSS class that sets display to “none”:

var new_table_node = $("placeholder_table").cloneNode(true);
new_table_node.id = "cloned_table";
Element.addClassName(new_table_node,"hidden");
$("grades_table").insert(new_table_node);

Later, when the table is transformed and ready to display, I take away the “hidden” CSS class, and — ta da! — here’s our transformed table, even in crusty old IE:

Element.removeClassName(new_table_node,"hidden");

I get to write a table in HTML, declaratively, so that what I’m coding looks like what I want produced; and then I can use powerful templating and node cloning tools to copy, modify, and display my table. When an asynchronous edit is made to one of the displayed grades (or a new one is added), the entire process can happen again: my hidden table is still there with its placeholders. I just clear out the cloned table, make another clone, and use my JSON data to bring it up to date.