Going Global

So far, the most popular posts I’ve written (if you discount the rant about syncing my mobile phone on Windows) are the two about getting dynamically loaded JavaScript code to execute in the global context.

A Bit of History

Just to recap, in Ajaxian Limitation I complained about the difficulty I encountered trying to get the JavaScript returned from an XmlHttpRequest to function correctly. After poking around a bit, I encountered a clever solution to this problem which I documented in The Magic Eval. Thanks to the help of lots of people, I think I can offer a final reduction of the problem and solution.

When you’re building an Ajax application, it can be extremely convenient to generate HTML on the server which gets sent back to the client. This is often faster and certainly more convenient, because you can share the same content templates as well as leverage the data in its native format rather than converted into JavaScript objects. But I often find that I want to send some code back in addition to the HTML. When you update the innerHTML attribute of an element, any script tags are not evaluated. So you have to manually evaluate the scripts.

Installing a Script Globally

When you receive your HTML snippet, you’ll need to extract the scripts — I typically use the String method extractScripts from prototype, but you can use whatever you’d like. Actually, extractScripts isn’t exactly perfect for the job, because it doesn’t seem to work with scripts with a src attribute. But that’s OK for this example.

Once I have the JavaScript that I’d like to add to the current browser context, I need to evaluate it. Unfortunately, while calling eval with the text of the script will execute the code, it won’t work quite as you expect: the code will execute in the scope of the eval statement and any functions you define won’t be available in the global page scope. There are some clever tricks around this, but they all require you to write your scripts in a slightly funky way. There’s one really clever trick that allows you to write your scripts just like you always do, but have everything work.

/** Execute a script in the global context. This installs all functions
    defined in this script into the global scope, unless they are
    explicitly created in different scopes.

    @param script   the source of the JavaScript to evaluate
 **/    
function installScript( script )
{
    if (!script)
        return;
    //  Internet Explorer has a funky execScript method that makes this easy
    if (window.execScript)
        window.execScript( script );
    else
        window.setTimeout( script, 0 );
}

This function takes advantage of a proprietary extension to the window object in Internet Explorer: the execScript method. There’s not much information on Microsoft’s MSDN page for the execScript method. But it seems to execute the script in the global scope and installs new functions into that scope, which is fortunate, because the approach used for every other browser doesn’t work in Internet Explorer.

For other browsers, the installScript function uses the setTimeout method of the window object to execute the script. Essentially, when setTimeout executes, it will evaluate the JavaScript in its first parameter. Fortunately, it evaluates the script in the global (or window) scope. This allows any functions you defined in that script to be available throughout the rest of the page.

Handling Script Tags with src Attributes

Sometimes returning inline JavaScript just isn’t the right solution. That means you’ll need to deal with script tags that include a src attribute. The gist of the solution is to pull out the value of the src attribute (either using regular expressions or straight string parsing) and load the JavaScript using an XmlHttpRequest. Once you have the script source, pass it to installScript and you’re done.

Comments

Pierre Tessier August 8th, 2006 @ 11:46 am

I have played with your solution, and have made it a little better. Essentially I took your installScript function, and made it so that it will work with any script tag including those which use a src= element to refer to an external script file. My function now takes the actual script DOM element as a parameter instead of the script code.


/** Execute a script in the global context. This installs all functions
    defined in this script into the global scope, unless they are
    explicitly created in different scopes.

    @param script   the actual script DOM element
 **/

function installScript( script )
{
    if (!script)
        return;

    if (script.src)
    {
        var head = document.getElementsByTagName("head")[0];
        var scriptObj = document.createElement("script");

        scriptObj.setAttribute("type", "text/javascript");
        scriptObj.setAttribute("src", script.src);  

        head.appendChild(scriptObj);

    }
    else if (script.innerHTML)
    {
        //  Internet Explorer has a funky execScript method that makes this easy
        if (window.execScript)
            window.execScript( script.innerHTML );
        else
            window.setTimeout( script.innerHTML, 0 );
    }
}

The inserting the element into the head tag of the document, was taken from liferay portal open source. They do it like that. The rest of the code comes from you.

I wanted to say thank you very much. Your post saved me mucho time, and will allow me to have a successful product launch. It may also end up in the Eclipse.org BIRT project.

PS: I have only tested the function in Internet Explorer 6, however liferay is cross-browser and it didn’t have anything in there to indicate browse compatibility issues.

Jeff Watkins August 8th, 2006 @ 7:58 pm

Pierre, I’m really glad this was able to help you out. There are too many times that we encounter a problem that we know must have a clean solution, but Googling for it doesn’t work, because no one has written about the solution.

That’s why I wrote up this particular post: solving the problem was such a pain in the arse, that I didn’t want anyone else to have to go through it. Sort of like my post on building Mozilla SpiderMonkey with NSPR support. These are the sort of things you should never have to solve for yourself, because you know someone else must have already solved it.

P.S. I hope you don’t mind me reformatting your code a bit… WordPress really isn’t the easiest tool for posting code samples.

Ashutosh Bijoor September 1st, 2006 @ 9:53 am

Hi
I came across your solution in time!

Thanks for the execScripts trick in IE.

However for other browsers, I found another trick.
I managed to make eval work in global context in FF and apparently in other browsers by using

eval.call(window,jscode)

Let me know if this works for you.

Tim D September 3rd, 2006 @ 8:29 am

Ashutosh — your eval.call approach worked wonders!! thanks. also thanks to Jeff for this googleable article. i was searching with: javascript eval js within a function into global context. -tim

Jeff Watkins September 3rd, 2006 @ 7:21 pm

Unfortunately, the eval.call trick doesn’t work in Safari. And I’ve no data on whether it works in Opera, since I’ve never run Opera (it’s just not a significant target for any of the Web apps I’ve built).

For cross-browser support, I’d stick with the execScripts IEism combined with window.setTimeout.

Brendan Baldwin September 5th, 2006 @ 4:55 pm

Oh HELL yes — I was giving up hope on IE’s setTimeout implementation where loading libraries like prototype.js etc…

This execScript crap TOTALLY fixed my problem. :^) Thanks!

pete mac September 14th, 2006 @ 8:53 am

great tip.
Thanks.

Misho September 14th, 2006 @ 6:34 pm

Hi guys - this article saved me a lot of time - I have the same problem with ajax - the scripts returned by the xmlhttpRequest are not working in ie. Can anyone tell how can I decide if certain script is installed or not without knowing what is inside?

Lewis September 27th, 2006 @ 5:49 pm

Just found this SOLUTION to my problems…thanks a lot!!!

Just like to give a little code back that I am using…Not checked for browsers other than IE! Maybe could do with tidying up…but it works!!

<html>
<head>
<script>
function loadXMLDoc(url,updateFunction)
{
    var req;
    var processReqChange = function processReqChange()
    {
        // only if req shows "complete"
        if (req.readyState == 4)
        {
            // only if "OK"
            if (req.status == 200)
            {
                if(updateFunction)
                    updateFunction(req.responseXML.documentElement.firstChild.nodeValue);
                else
                    updateDocument(req);
            }
            else
            {
                window.status = "There was a problem retrieving the XML data:n" + req.statusText;
            }
        }
    };
    // branch for native XMLHttpRequest object
    if (window.XMLHttpRequest)
    {
        req = new XMLHttpRequest();
        req.onreadystatechange = processReqChange;
        req.open("GET", url, true);
        req.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
        req.send(null);
        // branch for IE/Windows ActiveX version
    }
    else if (window.ActiveXObject)
    {
        req = new ActiveXObject("Microsoft.XMLHTTP");
        if (req)
        {
            req.onreadystatechange = processReqChange;
            req.open("GET", url, true);
            req.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
            req.send();
        }
    }
}
function updateDocument(req)
{
    var element = req.responseXML.documentElement;
    var id = element.attributes[0].value;
    var el = document.getElementById(id);
    var parent = el.parentElement;
    if(!parent)
        parent = el.parentNode;
    parent.innerHTML = unescape(req.responseText);

    var scripts = element.getElementsByTagName('script');
    for(i=0;i<scripts .length;i++)
    {
        var src = scripts[i].getAttribute('src');
        if(src)
            loadXMLDoc(src, compileScript);
        else {
            compileScript(scripts[i].firstChild.nodeValue);
        }
    }
}
function compileScript(script)
{
    if (window.execScript)
        window.execScript(script);
    else
        window.setTimeout(script,0);
}
</script>
</scripts></script></head>
<body onload="loadXMLDoc('http://localhost:8080/mc/b.jsp')">
    <div>
        <div id="b"></div>
    </div>
    <span onclick="doIt()">DoIt</span>
</body>
</html>

a.jsp:

< %@ page contentType="text/xml" %>

<script>
    function doIt()
    {
        alert('Hi I'm a.jsp');
    }
</script>

b.jsp:

< %@ page contentType="text/xml" %>

<script>
    function doIt()
    {
        alert('I'm b.jsp');
    }
</script>

Hello!

I use XML only….but easy to fix!

Thanks again

Lewis September 27th, 2006 @ 5:57 pm

…oops
div and script tags missing from post!

a.jsp has standard script tags…if you add src= then that is retrieved else body is used.

b.jsp is surounded by any tag with an id (div span etc) the code replaces any matching id in the dom…good for use with timers. Also, can run multiple threads at the same time!

Jeff Watkins October 1st, 2006 @ 3:38 pm

Lewis, I think I’ve fixed your formatting. I can’t quite get markdown to process the ASP tags correctly, but that’s life.

kirit November 2nd, 2006 @ 6:26 am

sorry …
though the explanation given above is prety much clear, but i still can’t apply to my problem
The problem is :
I have a page inside which i have a ‘div’ with a divID. I load evryth from the Ajax response object inside the div with the help of divId.
Now the scripts inside the response text is not working when the the page is loaded or even when that div is loaded.

How can i use execScript() in my this case. From where can I call this gloabal function ??
Plz help me !!
Plz !

Paul McLanahan November 22nd, 2006 @ 12:33 pm

Hi Jeff,

Your solution and find of the execScript function is excellent. I’m not sure if you’ve heard of or use the jQuery javascript library, but your fix has generated quite a discussion on the mailing list because the IE bug has been around for a while.

Since you helped in finding the cure, I just wanted to give a little back and tell you that a problem was discovered in your function. As it turns out, window.setTimeout(data,0) will be delayed for 10ms in Opera because there is a 10ms minimum for setTimeout. Granted, 10ms isn’t much time, but it could easily cause something you’re expecting to work fail due to the delay. In jQuery there is browser detection so that we can isolate safari if need be and the solution below uses that, but you could use any safari detection technique you wish.

Thanks again.

globalEval = function(data){
    if(window.execScript) // msie
        window.execScript(data);
    else if(jQuery.browser.safari) // safari detection in jQuery
        window.setTimeout(data,0);
    else // all others
        eval.call( window, data );
}
Jeff Watkins November 22nd, 2006 @ 12:47 pm

Paul, thanks for the note. I’m always glad to help. I had no idea that Opera was doing anything wacky like forcing a 10ms delay. That’s not nice. But, you probably have as good a solution as any — and I think the next Safari will fix the eval bug so the special case won’t be necessary.

Actually, I need to update this article to include the code I really use which distinguishes between browsers once and then always uses the same code.

Paul McLanahan November 22nd, 2006 @ 5:56 pm

That’d be excellent. Thanks again Jeff.

Alf Magne Kalleland December 3rd, 2006 @ 11:46 am

Thank you very much for the execScript idea Jeff. That saved my day:-)

Just a quick note on the Opera browser.

Opera doesn’t support innerHTML for the script tag. Instead you have to use the “text” attribute.

scriptTag.text

instead of

scriptTag.innerHTML

Gauthier Delamarre December 4th, 2006 @ 6:05 am

Alf, was just about to say same thing about IE7 ; mine throws exceptions when using scriptTag.innerHTML, and runs fine with scriptTag.text

However, it stills a problem : getElementsByTagName doesn’t return ALL script objects… only one of five is returned (in IE7 always, FF 2 reports 5 scripts, and everything’s ok)

PS : Alf, i’m precisely working on some of your scripts, i’ll send you the result of my work if you want :)

Kiran January 20th, 2007 @ 12:04 am

Hi Jeff,
First of all, I’d like to thank you for this discussion. I was really going crazy about how to make objects or functions defined through eval defined in global scope. I was trying to improve the security of the scripts on my site, some playing with Javascript when I was getting bored with my usual collage life, lolz. Unfortunately, the answer I found in this discussion did not work out fine for me!!
I used the following script, just a matter of testing.

function iS( script ) {
    if (!script) return;
    if (window.execScript) window.execScript(script);
        else window.setTimeout(script,0);
}
function evalGS() {iS("function bar(){return 'bar';}");}
function tF() {eval('bar();');}
evalGS();
tF();

Which, unfortunately, did not work out fine for me on my Firefox (v2.0.0.1). Well, tried working with
… eval(”bar=function(){return ‘bar’;}” …
No luck with that either.

And I don’t give up just like you… :P… So I just worked out a little. Tried out some other alternatives.Started with window._content.eval(), tried to check out the compatibility of this statement with other browsers. However, I found out later that to execute eval in Global Scope, we can just use window.eval() in Opera and this works out pretty fine on Firefox too.
So, my script now goes like this…

function iS( script ) {
    if (!script) return;
    if (window.execScript) window.execScript(script);
        else if (navigator.userAgent.indexOf('Safari')==-1) window.setTimeout(data,0);
            else window.eval(script);
}
function evalGS() {iS("bar = function(){return 'bar';}");}
function tF() {eval('alert(bar)');}
evalGS();
tF();

This worked fine for me on IE, FF, Opera.
Please verify it for yourself :).
I havent tested it on Safari, However, I would be glad if you could test it for me.

Anyway thanks again for this discussion. It was great :).

Kiran January 20th, 2007 @ 5:17 am

Sorry, but I posted that wrong.
Heres the script. Lolz.

function iS( script ) {
    if (!script) return;
    if (window.execScript) window.execScript(script);
    else if (navigator.userAgent.indexOf('Safari')!=-1) window.setTimeout(script,0);
    else window.eval(script);
}
function evalGS() {iS("bar = function(){return 'bar';}");}
function tF() {eval('alert(bar)');}
evalGS();
tF();

Just a ’small’ mistake. :P

[…] (PS: Having found the magic term execScript, I was then able to find some related articles on this topic by Dean Edwards and Jeff Watkins. However much of the details are buried in the comments, so I hope this article will increase both the findability and conciseness of this information). […]

Matthew Williams February 1st, 2007 @ 8:45 am

This is exactly what I need, however, I cannot determine where it needs to be executed. I have a page with a varity of DIVs that get populated via AJAX.Updater calls using Prototype.

I populate one DIV with another DIV and when I try to populate that with a Dojo widget; it doesn’t render and I just get plain text. Between this and the recent article on Ajaxian, I know the solution is right under my nose, I just can’t get it implemented.

A bit new to JavaScript but frameworks such as Prototype, Moo Tools, OpenRico and Dojo make it so easy to make a really slick site.

If you’d like to offer a helping hand, please contact me directly:
matthew.d.williams [at] gmail.com

XoraX February 20th, 2007 @ 11:42 am

all code is not really compatible with Safari.
all variable declared in global is not accessible directly after (in the function who call the eval global).

exec this with safari :

global eval

var screentest = function (s, w){
  var t = 'bug';

  if(w){
    var Start = (new Date()).getTime();
    while(typeof test != 'function') {
      if( ((new Date()).getTime() - Start) > 6000 ){
        addconsole('infinite while: test() is '+typeof(test) +' after '+ ((new Date()).getTime() - Start) +'ms on '+s);
        break;
      }
    }
  }
  try{
    t = test(s);
    addconsole(t);
    return true;
  } catch (e) {
    addconsole('error: '+e+ ' -- on -- '+s);
    return false;
  }
}

var addconsole = function (s){
  document.getElementById('console').innerHTML += '' +s;
}

function globaleval (){
  var s = "var test = function (s){ return 'test() running in global scope: '+s;};";
  if(window.execScript){
    window.execScript(s);
    if(screentest('window execScript'))
      return;
  }
  if(window.eval) {
    window.eval(s);
    if(!screentest('window eval')){
      window.setTimeout(s,0);
      if(!screentest('setTimeout eval',true)){
        addconsole('last try with setTimeout(screentest)...');
        setTimeout('screentest("setTimeout eval and called with setTimeout");',0);
      }
    }
  }
}

– console –

or view online at http://www.xorax.info/test/eval.php

uomo February 25th, 2007 @ 11:55 pm

The information I found here was rather helpful. Thank you for this.

siralucard_1 April 20th, 2007 @ 10:50 pm

Im not in the mood to write a long explanation, so those who where experiencing the problem that IE didnt exec the extrernal ‘ajaxed’ javascripts, well, I found something interesting (It is something with the innerHTML).
If you add something like this to the innerHTML=responseText line will fix the problem (im not an expert so i really dont know why a newline added to the innerHTML fixes it):

...

container.innerHTML = “”+ajax.responseText

Hope this helps anybody, it worked for me… after l
LONG LONG hours(DAMN IE).

siralucard_1 April 20th, 2007 @ 11:00 pm

sorry, im kinda new here so, i didnt know how to write html code.. thats why the “”+ajax.responseText is wrong written, it should display:

“<br style=’line-height:0px !important;’ />”+ajax.responseText