Making Javascript DOM a Piece of Cake with the graft() Function
In an earlier article I presented DOM as a useful way to construct documents on the fly in Javascript. DOM is well known for this purpose, but it’s also well known for being extremely irritating and verbose to use in Javascript. Some programmers may be tempted to use quoted HTML combined with the innerHTML property, which may (for them) be somewhat easier to crank out than DOM code. But this is hardly a way to go forward, and creates all sorts of problems when using AJAX, JSON, or when working in an environment involving a lot of strict XML usage. Casting all of that aside, this article will demonstrate the use of the graft() function to simplify the construction of DOM structures using Javascript’s own simple object notation.
More after the jump..
Where’s the DOM Leap?
Those of you who have watched AJAX and JSON gain increasing popularity in the last couple of years have probably been scratching your heads wondering where the corresponding leap in DOM productivity was. Due to the rather slow evolution of web browsers when it comes to Javascript and DOM (possibly with the notable exception of the inclusion of Javascript 1.7 in Firefox 2), we may not see any wide support for new built in methods of improving our web application code. In the meantime, we have to look elsewhere.
To our rescue comes Sean Burke, author of the Higher Order Javascript document, who has styled HOJ as a sort of companion to the book Higher Order Perl
. In the document, Burke defines a useful function called graft() which allows us to build chunks of DOM documents using really simple object notation. Graft() will feel very familiar to anyone who makes extensive use of the JSON format or who has worked with LISP.
Introduction to graft()
I’ll dive right into an example here and show graft in its simplest use. Suppose we have a DOM element somewhere in our document that we want to insert stuff into:
<div id="mycontentdiv"></div>
If we wanted to insert a button into the above div element, we could use standard DOM in the following manner:
var mydiv = document.getElementById("mycontentdiv");
var btn = document.createElement("input");
btn.type = "button";
btn.value = "click me";
btn.onclick = function(){this.value="i have been clicked!";};
mydiv.appendChild(btn);
Frankly, this is a lot of code to write to create a simple button. Burke’s graft() function allows us to create DOM objects with the following form:
graft(domElementReference,['nodeName',{attributes_hash}, ... ]);
Where ‘…’ is simply additional children or subtrees in the same format. Thus the above button example becomes the following:
graft(
document.getElementById("mycontentdiv"),
["input",
{
value:'click me',
type:'button',
onclick:'this.value="i have been clicked!";'
}
]
);
Of course, DOM structures are trees so we would like to be able to assemble objects which are as complex as DOM allows. Graft() will traverse whatever tree you give it, and make such Javascript DOM objects tremendously easy to read compared to their classically-defined counterparts. Take for example, this example of typical DOM code to build a table:
var tbl = document.createElement('table');
tbl.setAttribute("border",1);
tbl.setAttribute("cellpadding",0);
tbl.setAttribute("cellspacing",0);
var tr = document.createElement("tr");
var td = document.createElement("td");
td.setAttribute('valign','bottom');
td.appendChild(document.createTextNode("Graft()"));
tr.appendChild(td);
td = document.createElement("td");
td.appendChild(document.createTextNode("makes"));
tr.appendChild(td);
td = document.createElement("td");
td.appendChild(document.createTextNode("this"));
tr.appendChild(td);
td = document.createElement("td");
td.setAttribute('style','background-color:#ff0;');
td.appendChild(document.createTextNode("easier"));
em = document.createElement("em");
em.setAttribute('style','font-size:32px;');
em.appendChild(document.createTextNode(" to do.."));
td.appendChild(em);
tr.appendChild(td);
tbl.appendChild(tr);
document.getElementById("mycontentdiv").appendChild(tbl);
The above makes for fairly unreadable code which will impede maintenance. Graft gives us a far more natural way of expressing the above table:
graft(
document.getElementById("mycontentdiv"),
["table",
{border:1,cellpadding:0,cellspacing:0},
['tbody',
['tr',
['td',
{valign:'bottom'},
"Graft()"
],
['td',
"makes"
],
['td',
"this"
],
['td',
{style:'background-color:#ff0;'},
"easier",
["em",
{style:'font-size:32px;'},
" to do.."
]
]
]
]
]
);
As you can see, this allows us to still use the DOM if we want to generate objects with visual structure, but without the hassle of quoted HTML injected into the innerHTML property. It also allows us to simplify bits of Javascript which perform lots of looping and have to construct DOM objects iteratively, and possibly employ the results of other functions. Say we have some code that constructs a table with many rows and columns like this:
Constructing DOM trees with in loops with graft().
Graft makes DOM object creation code much easier to read, and also allows easier-to-read assembly of such structures in loops.
function getRow2(i,j)
{
var td = document.createElement("td");
var strong = document.createElement("strong");
strong.appendChild(document.createTextNode(i*j));
td.appendChild(strong);
return td;
}
var tbl = document.createElement('table');
tbl.setAttribute("border",1);
tbl.setAttribute("cellpadding",0);
tbl.setAttribute("cellspacing",0);
for(var i=0;i<25;i++)
{
var tr = document.createElement("tr");
for(var j=0;j<25;j++)
{
tr.appendChild(getRow2(i,j));
}
tbl.appendChild(tr);
}
document.getElementById("mycontentdiv").appendChild(tbl);
This sort of code can quickly balloon in size fairly quickly and makes for many pages of drudgery. We can clean it up with graft and make it much easier to understand what is going on:
function getRow1(i,j)
{
return ['td',['strong',i*j]];
}
var tbl = ['table',{border:1,cellpadding:0,cellspacing:0}];
var tbody = ['tbody'];
for(var i=0;i<25;i++)
{
var tr = ['tr'];
for(var j=0;j<25;j++)
{
tr[tr.length] = getRow1(i,j);
}
tbody[tbody.length]=tr;
}
tbl[tbl.length] = tbody;
graft(document.getElementById("mycontentdiv"),tbl);
The resulting code is much nicer. Another graft() advantage is that many Javascript programmers (coming in with previous experience from other languages strong in array or list manipulation, ranging LISP or PHP) will prefer to manipulate/construct their documents as arrays instead of messing around with childNodes, firstChild, nextChild, replaceNode, insertBefore and other seemingly obscure DOM stuff.
Graft() Source Code
Here with no further ado, I give you the source code to Sean Burke’s graft() function.
// graft() function
// Originally by Sean M. Burke from interglacial.com
function graft (parent, t, doc) {
// Usage: graft( somenode, [ "I like ", ['em',
// { 'class':"stuff" },"stuff"], " oboy!"] )
doc = (doc || parent.ownerDocument || document);
var e;
if(t == undefined) {
throw complaining( "Can't graft an undefined value");
} else if(t.constructor == String) {
e = doc.createTextNode( t );
} else if(t.length == 0) {
e = doc.createElement( "span" );
e.setAttribute( "class", "fromEmptyLOL" );
} else {
for(var i = 0; i < t.length; i++) {
if( i == 0 && t[i].constructor == String ) {
var snared;
snared = t[i].match( /^([a-z][a-z0-9]*)\.([^\s\.]+)$/i );
if( snared ) {
e = doc.createElement( snared[1] );
e.setAttribute( 'class', snared[2] );
continue;
}
snared = t[i].match( /^([a-z][a-z0-9]*)$/i );
if( snared ) {
e = doc.createElement( snared[1] ); // but no class
continue;
}
// Otherwise:
e = doc.createElement( "span" );
e.setAttribute( "class", "namelessFromLOL" );
}
if( t[i] == undefined ) {
throw complaining("Can't graft an undefined value in a list!");
} else if( t[i].constructor == String ||
t[i].constructor == Array ) {
graft( e, t[i], doc );
} else if( t[i].constructor == Number ) {
graft( e, t[i].toString(), doc );
} else if( t[i].constructor == Object ) {
// hash's properties => element's attributes
for(var k in t[i]) e.setAttribute( k, t[i][k] );
} else {
throw complaining( "Object " + t[i] +
" is inscrutable as an graft arglet." );
}
}
}
parent.appendChild( e );
return e; // return the topmost created node
}
function complaining (s) { alert(s); return new Error(s); }
For production use I suggest you work out a convenient way of handing errors.
Extending graft() with support for Closures/Functions
In one of the examples above, when converting some DOM code to graft I had to “downgrade” a function attachment from this:
btn.onclick = function(){this.value="i have been clicked!";};
to this:
onclick:'this.value="i have been clicked!";'
As you might have guessed if you’ve followed previous Javascript articles here at Schadenfreude, we’re not finished yet.
One thing that is missing from Burke’s version of graft() is support for attaching functions to DOM objects as we construct them. Being able to do so would allow us to throw together DHTML interfaces very quickly and attach functions (as well as attach closures ) to DOM objects. To add this support, we only need to modify the loop where attributes are assigned from the property hash, changing this:
// hash's properties => element's attributes
for(var k in t[i]) e.setAttribute( k, t[i][k] );
to this:
// hash's properties => element's attributes
for(var k in t[i]) {
// support for attaching closures to DOM objects
if(typeof(t[i][k])=='function'){
e[k] = t[i][k];
} else {
e.setAttribute( k, t[i][k] );
}
}
Here is our final extended version of the graft() function:
// graft() function
// Originally by Sean M. Burke from interglacial.com
// Closure support added by Maciek Adwent
function graft (parent, t, doc) {
// Usage: graft( somenode, [ "I like ", ['em',
// { 'class':"stuff" },"stuff"], " oboy!"] )
doc = (doc || parent.ownerDocument || document);
var e;
if(t == undefined) {
throw complaining( "Can't graft an undefined value");
} else if(t.constructor == String) {
e = doc.createTextNode( t );
} else if(t.length == 0) {
e = doc.createElement( "span" );
e.setAttribute( "class", "fromEmptyLOL" );
} else {
for(var i = 0; i < t.length; i++) {
if( i == 0 && t[i].constructor == String ) {
var snared;
snared = t[i].match( /^([a-z][a-z0-9]*)\.([^\s\.]+)$/i );
if( snared ) {
e = doc.createElement( snared[1] );
e.setAttribute( 'class', snared[2] );
continue;
}
snared = t[i].match( /^([a-z][a-z0-9]*)$/i );
if( snared ) {
e = doc.createElement( snared[1] ); // but no class
continue;
}
// Otherwise:
e = doc.createElement( "span" );
e.setAttribute( "class", "namelessFromLOL" );
}
if( t[i] == undefined ) {
throw complaining("Can't graft an undefined value in a list!");
} else if( t[i].constructor == String ||
t[i].constructor == Array ) {
graft( e, t[i], doc );
} else if( t[i].constructor == Number ) {
graft( e, t[i].toString(), doc );
} else if( t[i].constructor == Object ) {
// hash's properties => element's attributes
for(var k in t[i]) {
// support for attaching closures to DOM objects
if(typeof(t[i][k])=='function'){
e[k] = t[i][k];
} else {
e.setAttribute( k, t[i][k] );
}
}
} else {
throw complaining( "Object " + t[i] +
" is inscrutable as an graft arglet." );
}
}
}
parent.appendChild( e );
return e; // return the topmost created node
}
function complaining (s) { alert(s); return new Error(s); }
I hope you’ve enjoyed this quick introduction to graft().
Further reading:
About this entry
- Author:
- Maciek
- Published:
- 15/10/2006 at 13h42
- Category:
- Posted in Javascript, DOM
- Tags:
- Tags DOM, graft, Higher, javascript, Order
