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


Comments

  1. Avatar

    klevo

    Posted: 1 day later:

    Awesome article. Thank you! This will save me a lot of time in the future.

  2. Avatar

    James Cook

    Posted: 2 days later:

    We’ve been using this approach for a while on a website we are developing. We are using the code described here in March 2006:

    http://mg.to/topics/programming/javascript/jquery

  3. Avatar

    hoeken

    Posted: 2 days later:

    I really liked this article. I’m going to be getting into alot of AJAX stuff soon and I really like this alot!

    Hopefully the guys at Yahoo will too.

  4. Avatar

    Binny V A

    Posted: 2 days later:

    I have created a function extremely similar to this one… http://www.openjs.com//scripts/createdom/

    Nice solution – I had a problem with multiple similar tags. The solution to that problem made the whole script look like a hack.

  5. Avatar

    troels

    Posted: 2 days later:

    Mochikit implements an almost identical api : http://mochikit.com/doc/html/MochiKit/DOM.html

  6. Avatar

    Kumar S

    Posted: 2 days later:

    Excellent. Will help more in coming days. Life saver function

  7. Avatar

    Mike Holloway

    Posted: 2 days later:

    I’ve been using the same link as James Cook said, but this article is still amazing. Totally efficient.

  8. Avatar

    Daniel

    Posted: 2 days later:

    scriptaculous’ Builder works in the exact same manner: http://wiki.script.aculo.us/scriptaculous/show/Builder

  9. Avatar

    Nicholas

    Posted: 3 days later:

    For me it doesn’t work properly on IE (6, 5.5, 5.0). Any property inside “{}” works. Anybody got this same bug?

  10. Avatar

    Bramus!

    Posted: 3 days later:

    Awesome function! Nice work!

  11. Avatar

    the way to truth is unique

    Posted: 3 days later:

    I have created a similar construct for my library [1] months ago

    ” _.GLUE

    which mostly behaves like graft:

    [1] http://sardalya.pbwiki.com/Shortcuts/

    [2] demo: http://www.sarmal.com/sardalya/testcase/DOMCreate_test.html

    I think I can have some further insight from the code.

    Thanks for your post.

    Cheers; Volkan

  12. Avatar

    Weavil

    Posted: 3 days later:

    To get the above table example to work in IE you need to include the tbody tag.

    So at the end of the row instead of:

    tbl[tbl.length]=tr;

    You should have:

    tbody[tbody.length]=tr;

    And at the end of the table instead of:

    tbl[tbl.length]=tr;

    Have:

    tbl[tbl.length]=tbody;

    This is working in both FF 1.5 and IE 6.0.

  13. Avatar

    Frederic Torres

    Posted: 3 days later:

    Maciek

    This insertion of the table is not correctly working with IE6 . The table is in the dom but not displayed. It is working with firefox.

    I tried to play with the style.display with no result.

    In case you have the solution.

    Frederic Torres www.InCisif.net Web Testing with C# or VB.NET

  14. Avatar

    Shaun Newman

    Posted: 3 days later:

    Great article, not sure if the code samples were trimmed on the right hand side though, looked like a few bits off the end of a line went missing.

    Thanks to all those posting other links to similar functions, I learned an important lesson. Don’t just use a library, look at it in depth and understand it’s inner workings, things make more sense that way ;0)

  15. Avatar

    Maciek

    Posted: 3 days later:

    Thanks for the comments guys. I’ve updated the examples with some slight fixes for Internet Explorer.

  16. Avatar

    Nicholas

    Posted: 3 days later:

    My problem is not only for table elements. Every element with attributes insite “{}” doesn’t work. I tried this:

    graft( gElm(‘testDiv’), [‘b’, {style:’color:red;’}, ‘123’ ] );

    The tag was wrote but the color kept black(default). Did anybody got the same problem ?

  17. Avatar

    cdude

    Posted: 6 days later:

    the reason the style won’t work in IE is because of its bug.

    http://www.quirksmode.org/bugreports/archives/2005/03/setAttribute_does_not_work_in_IE_when_used_with_th.html

    change the ‘style’ attribute value to an object instead of a string, modify the function to alter element.style instead. For example:

    var table = [‘table’, {border: 1, cellspacing: 3, style: {width: ‘100%’}}, [‘tbody’, [‘tr’,

    [‘td’, {style: {backgroundColor: ‘blue’, color: ‘white’}}, ‘hahaha’] ] ] ];

    for(var k in t[i]) { if(k 'style') { Object.extend(e.style, t[i][k]); // do a full loop if you're not using prototype } else if(typeof(t[i][k])‘function’){ e[k] = t[i][k]; } else { e.setAttribute( k, t[i][k] ); } }

  18. Avatar

    cdude

    Posted: 6 days later:

    doh, indentation is not preserved.

    the code should read: if(k == ‘style’). The comparator was removed :-?

  19. Avatar

    Nicholas

    Posted: 7 days later:

    Nice tip! Tks a lot!

  20. Avatar

    maYO

    Posted: 9 days later:

    Looks interesting. Though setting up events like onclick doesn’t work in IE browsers, it is a very useful function nonetheless.

  21. Avatar

    maYO

    Posted: 9 days later:

    Whoops, sorry. My bad, should have RTFM!

  22. Avatar

    maYO

    Posted: 9 days later:

    Just found out that setting CSS classes isn’t working in IE browsers as they don’t really get the setAttribute method.

    Here’s a fix for 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 { if (k == “class”) e.className = t[i][k]; e.setAttribute( k, t[i][k] ); } }

    When trying to set the class, instead of using the setAttribute method, I just set the className value. Works great for me.

  23. Avatar

    Maciek

    Posted: 10 days later:

    Thanks again for the comments, it looks like I’m going to have to revisit the function and implement some fixes.

About

    Buildingsky.net is comprised of Corban Brook and Maciek Adwent. We build experimental web applications.

    We are interested in computer science, ruby-lang, javascript, web technologies, audio synthesis, finance/economics.

Contact

Projects

Categories