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.

24 Comments

  1. Pierre Tessier
    Posted 8 Aug 2006 at 11:46 am | Permalink

    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.

  2. Posted 8 Aug 2006 at 7:58 pm | Permalink

    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.

  3. Posted 1 Sep 2006 at 9:53 am | Permalink

    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.

  4. Tim D
    Posted 3 Sep 2006 at 8:29 am | Permalink

    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

  5. Posted 3 Sep 2006 at 7:21 pm | Permalink

    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.

  6. Posted 5 Sep 2006 at 4:55 pm | Permalink

    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!

  7. pete mac
    Posted 14 Sep 2006 at 8:53 am | Permalink

    great tip. Thanks.

  8. Misho
    Posted 14 Sep 2006 at 6:34 pm | Permalink

    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?

  9. Lewis
    Posted 27 Sep 2006 at 5:49 pm | Permalink

    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

  10. Lewis
    Posted 27 Sep 2006 at 5:57 pm | Permalink

    …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!

  11. Posted 1 Oct 2006 at 3:38 pm | Permalink

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

  12. kirit
    Posted 2 Nov 2006 at 6:26 am | Permalink

    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 !

  13. Paul McLanahan
    Posted 22 Nov 2006 at 12:33 pm | Permalink

    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 );
    }
    
  14. Posted 22 Nov 2006 at 12:47 pm | Permalink

    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.

  15. Paul McLanahan
    Posted 22 Nov 2006 at 5:56 pm | Permalink

    That’d be excellent. Thanks again Jeff.

  16. Posted 3 Dec 2006 at 11:46 am | Permalink

    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

  17. Gauthier Delamarre
    Posted 4 Dec 2006 at 6:05 am | Permalink

    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 :)

  18. Kiran
    Posted 20 Jan 2007 at 12:04 am | Permalink

    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 :).

  19. Kiran
    Posted 20 Jan 2007 at 5:17 am | Permalink

    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

  20. Matthew Williams
    Posted 1 Feb 2007 at 8:45 am | Permalink

    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

  21. Posted 20 Feb 2007 at 11:42 am | Permalink

    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

  22. Posted 25 Feb 2007 at 11:55 pm | Permalink

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

  23. Posted 20 Apr 2007 at 10:50 pm | Permalink

    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).

  24. Posted 20 Apr 2007 at 11:00 pm | Permalink

    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

One Trackback

  1. [...] (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). [...]