Ajaxian Limitation

I was just thinking about adding another Ajax-y feature to the site when it occurred to me: I can either return JavaScript or HTML but not both.

Typically when returning HTML using an XMLHttpRequest object I set the innerHTML property of a div on the page with the result (provided the operation was successful). But I’m willing to bet that any scripts contained in that HTML doesn’t get interpreted.

Let’s find out…

Ajax Is All About Rich Content

When I first started writing Web applications back in 1995, “rich content” meant anything with images, sound, and words. Behaviour didn’t enter into it because scripting wasn’t really possible, and even when it became possible with the first generation of Javascript, there wasn’t really much you could accomplish with it.

Today, “rich content” is almost synonymous with dynamic content. Behaviour is almost a required feature of rich content. So it seems reasonable that you might want to return rich content from your XMLHttpRequest.

Problems Returning DHTML from XMLHttpRequest

Most of my early uses of XMLHttpRequest simply return HTML. No script elements. No behaviour. Static, non-rich content. But that means the behaviour of your Web application is fixed. You must load all the Javascript include files you might ever need when the browser first parses your HTML file. That’s not a good recipe for modular software design.

In the past, when the XMLHttpRequest completed, I would take the responseText and assign that to the innerHTML of a div on my page. Like so:

function requestComplete( request )
{
    var results= document.getElementById( "htmlResult" );
    if (!results)
        return;

    results.innerHTML=request.responseText;
}

Unfortunately, this will not process any script tags contained within the responseText. Check out this example of storing an HTML fragment with a script tag to the innerHTML property. Were the script tag processed, we’d see an alert box. No alert box, therefore, script tag not processed.

Alternatives to Fetching DHTML

There are two simple alternatives that will solve this problem admirably.

  • Don’t worry about it. Maybe your Web application just isn’t complicated enough to warrant a modular approach. Most aren’t that complicated. There’s no sense building a solution to a problem you don’t have. I really mean this.

  • Separate your behaviour. You can easily fetch just a Javascript file to expand your application’s behaviour. It won’t have HTML content, but you can always fetch that separately. Two calls to XMLHttpRequest means two chances for failure and additional latency. So this might only be a good solution over a robust, high-speed network (not the Internet).

I’m not known for taking the easy way out. I want to solve this problem.

Evaluating script Tags

It would seem the answer is to evaluate the script tags in the response from XMLHttpRequest. This shouldn’t be too hard. The first step is to evaluate each script tag in the response:

function requestComplete( request )
{
    var results= document.getElementById( "htmlResult" );
    if (!results)
        return;

    results.innerHTML=request.responseText;
    var children= results.children;
    var child;
    var index;

    for (index=0; index<children.length; ++index)
    {
        child= children[index];
        if (child.tagName && "SCRIPT"==child.tagName)
        {
            eval( child.innerHTML );
        }
    }
}

Unfortunately, this doesn’t propagate functions defined in the fetched DHTML to the global scope. That’s an unacceptable limitation.

Giving Up, For Now…

I’ve tried numerous ways to massage the script tags into something that will work correctly, but the only solutions mean writing weird and wacky Javascript. I hate weird and wacky syntax.

Worse, the weird and wacky syntax doesn’t really address the following example:

<script type="text/javascript">
    function tabLabelWasClicked()
    {
        alert( "The tab label was clicked!" );
    }
</script>
<span id="tabLabel" onclick="tabLabelWasClicked()">Tab 1</span>

Yes, I can transform the code in the script tag so that it will return an object that includes the tabLabelWasClicked function, but then I’ll have to transform the onclick handler to call the same function. But that means every script tag will need a unique container object. That’s just a pain.

So, I’m going to give up on this problem for now. It’s just a problem. This doesn’t mean you can’t expand the behaviour of your Web application by dynamically loading Javascript, but it does mean you’ll have to separate your behaviour and content. The unobtrusive Javascripter guys will be delighted.

Update: The discussion continues in Going Global.

Comments

Jeff Watkins August 25th, 2005 @ 9:24 am

Not being the sort who gives up easily, I’ve been working on a solution to this limitation of the Ajax pattern. I think I’m almost there. Maybe later today or tomorrow I’ll have something to report.

Jeff Watkins August 26th, 2005 @ 8:16 am

OK, so the problem is solved. But the solution has prompted me to write an entire article on eval rather than just post the solution.

I hope to get the eval article posted this weekend.

Wayne Blair August 31st, 2005 @ 11:52 pm

Well - this has been irritating me too. I found one way to make the eval technique work. In a javascript loaded into global scope at page load, add a class constructor function (e.g. function global(){}). Then declare and call all your functions in the dynamically evaled script as members of the class prototype:

// arbitrary script
global.prototype.foo = function() { return ‘bar’;};

// now this will work in any scope
alert(global.prototype.foo());

Wish it were prettier, but it’s the best I’ve come up so far. Good thread you started. (P.S. - if the one for your article is prettier, please drop a hint).

Jeff Watkins September 1st, 2005 @ 1:45 pm

Wayne, I think you’ll like the solution I’ve worked out. Check out The Magic Eval.

I’ll update this page (soon) with a sample using this technique.

Gabriel Harrison October 11th, 2005 @ 5:30 pm

The folks over at OpenRico (http://www.openrico.org) rely on a public open source library called prototype.js that I find to be very nice when dealing with Ajax and other JavaScript related functionality. One of the classes defined in their library is Ajax.Updater. This object will not only execute any script tag blocks returned in a chunk of Ajax loaded HTML it will also not dump them in any of the DIV blocks or other innerHTML targets you choose; this effectively hides the code. Two birds, one stone.

Browse on over to the prototype.js homepage
http://prototype.conio.net

Also check out the excellent work done by the fellows at OpenRico
http://openrico.org

Jeff Watkins October 11th, 2005 @ 5:35 pm

Thanks Gabriel! These are both resources I’d heard of before (in fact I’ve used some of the prototype.js code once or twice) but haven’t really made a big deal about. I’m glad you mentioned them.

Mark-Shamus McCahey May 30th, 2006 @ 10:48 pm

Here ya go… will handle blocks of javascript and tags which just link to .js files via src:

——- BEGIN SAMPLE ——-

|

function newXMLHttpRequest () {
var httpRequest = false;
if (window.XMLHttpRequest) { // Mozilla, Safari,…
httpRequest = new XMLHttpRequest();
if (httpRequest.overrideMimeType) {
httpRequest.overrideMimeType(’text/xml’);
}
} else if (window.ActiveXObject) { // IE
try {
httpRequest = new ActiveXObject(”Msxml2.XMLHTTP”);
} catch (e) {
try {
httpRequest = new ActiveXObject(”Microsoft.XMLHTTP”);
} catch (e) {}
}
}
return httpRequest;
}

function loadInnerHTML (elementId, fileName) {
var req = newXMLHttpRequest();
if (req) {
req.onreadystatechange = function() {
if (req.readyState == 4 && (req.status == 200 || req.status == 304)) {
var fileExtension = fileName.substring(fileName.lastIndexOf(”.”));
if (fileExtension == ‘.js’) {
// this .js? sweet. eval the lot!
eval (req.responseText);
} else {
// no .js extension? no problem! let’s get our hands dirty
var el = document.getElementById(elementId);
var head = document.getElementsByTagName(”head”)[0];
var re = /([\s\S]*?)

——- END SAMPLE ——-

Mark-Shamus McCahey May 30th, 2006 @ 10:57 pm

Wow that paste attempt really bit the dust. figured a nerd’s comment system would allow for pastebin-style… er… pasting =].

Here’s a link to the code I tried to paste above:
http://www.derosion.com/uploads/loadInnerHTML.txt

Most aspects of that solution were gleaned from elsewhere; suffice to say I claim no credit for the code itself, but this particular implementation along with the srcStr fork to handle both types of script tags (those with or without an src property) is, to my knowledge, a first.

Jeff Watkins May 31st, 2006 @ 6:55 am

Sorry about the paste problem. I really haven’t customised WordPress to any great degree, so comments are basically what is offered stock. You can use Markdown formatting commands, however.

I checked out your code and it looks pretty clean. Mostly these days I’ve been sticking with scriptaculous and prototype for most of this sort of thing.

One of the things I noticed about your code is that the eval statement won’t get executed at the global scope. Therefore, it can’t inject functions in to the page that are callable from anywhere else. Yes, the script can do its thing, but it will essentially disappear afterwards.

Also, I’ve found that injecting a new script tag into the head when running under Microsoft Internet Explorer simply fails. However, I’ve never tried setting the defer attribute to true. I’ll have to see if that makes any difference.

natasha July 29th, 2006 @ 8:20 am

function go(){
eval(”bar = function(){return ‘bar’}”);
eval(”var a = 22;”);
eval(”b = 22;”);
}

function go1(){
var b = 33;
}

go();
go1();

alert(bar); Ok
alert(b); Ok
alert(a); does not work

So if you are defining global variables without var
and i you are using function declaration a=function(){} works in all browsers;

Jeff Watkins July 29th, 2006 @ 8:45 am

Natasha, I hadn’t tried leaving off the var keyword. That’s an interesting feature of eval.

On the other hand, this does require that you rewrite scripts to work with this technique, which is a bit of a burden.

Still, it’s good to know these quirks.