November 11, 2009

Frames without frames. AJAH selective insertion

And W3C said Let there be frames, and there was frames, and they were used (and abused) and it was good, and W3C said This is right (we're gonna deprecate them soon) and there was much rejoicing.

Is not new that web developers hate all that's made with frames. They're a burden nobody wants to carry, so confusing, so "nineties", so... well, deprecated, that's it. So you may think I'm just delving into the past. Frames, as we know it, are dissapearing into extinction but, the main idea, more or less, is still needed nowadays.

AJAH!

I'm talking about AJAH web applications. In these, the default click action is overriden with Javascript, making an asynchronous request to the URI and inserting the HTML result into a DOM object. It's simpler with a picture of a sample layout:

<h1>#Header</h1>
<u1>#Menu</u1>
<div>#App</div>

When you click on a link in #menu, you want to async call the URL and set the result as the content of #application. This can be done setting the XMLHttpRequest onreadystatechange event more or less like this:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if(this.readyState === 4 && this.status === 200) {
        document.getElementById('App').innerHTML = this.responseText
    }  
};
xhr.open('GET', url, true); // the url of the request
xhr.send(null)

Wasn't it easy? Now, if your request returns "<h2>Option 1</h2>" the content of the App DOM Object changes.

One request, multiple changes

How about changes in more than one "frame"? Let's say you want to modify the contents of App and set Footer to a status text. First you must modify your application to wrap the result within the appropriate tags:

<div id="App">This must go inside the App container</div>
<p id="Footer">This is the new stats of the application, and goes into the Footer</p>

In the onreadystatechange event, create an anonymous node and set the response as innerHTML.

...
var node = document.createElement('div');
node.innerHTML = this.responseText;
...

Then, iterate over its direct children. If the child has an id, search the real DOM with getElementById, replacing the node, when found. If the child has no id or doesn't match to a node in the document, append it to a default DOM object, say body (or whatever fits in your case).

var child = node.childNodes;
var default = document.getElementsByTagName('body')[0];
while(child.length) {
    if(child[0].id) {
        var replacement = document.getElementById(child[0].id);
        if(replacement) {
            replacement.parentNode.replaceChild(child[0], replacement);
            continue;
        }
    }
    default.appendChildren(child[0])
}

jQuery oversimplification

This could be achived easily with jQuery:

$(this.response)
    .filter(function(){
        var u = document.getElementById(this.id);
        return !(u && u.parentNode && u.parentNode.replaceChild(this, u))
    })
    .appendTo('body')

Nice one, isn't it?