JS includes - the saga continues…

The problem in question is how to find out a dynamically included JavaScript file is actually loaded. The concept of JavaScript includes is here, the IE-only solution is here. The IE solution is to use the onreadystatechange event that is fired when a new script is included. It also works for dynamically loaded CSS files using a new link DOM element. Thanks to the comment from Björn Graf, I tried using onload event to test if the new script is included using Firefox. It worked!

The code

What we have here (demo) is trying to include a .js file and an .css file, creating new script and link DOM elements. Then I'm attaching event listeners to those new elements - one onload and one onreadystatechange. The script that is included (jsalert.js) has one alert().

var css;
function include_css(css_file) {
    var html_doc = document.getElementsByTagName('head')[0];
    css = document.createElement('link');
    css.setAttribute('rel', 'stylesheet');
    css.setAttribute('type', 'text/css');
    css.setAttribute('href', css_file);
    html_doc.appendChild(css);

    // alert state change
    css.onreadystatechange = function () {
        if (css.readyState == 'complete') {
            alert('CSS onreadystatechange fired');
        }
    }
    css.onload = function () {
        alert('CSS onload fired');
    }
    return false;
}

var js;
function include_js(file) {
    var html_doc = document.getElementsByTagName('head')[0];
    js = document.createElement('script');
    js.setAttribute('type', 'text/javascript');
    js.setAttribute('src', file);
    html_doc.appendChild(js);

    js.onreadystatechange = function () {
        if (js.readyState == 'complete') {
            alert('JS onreadystate fired');
        }
    }

    js.onload = function () {
        alert('JS onload fired');
    }
    return false;
}

Results

As you can probably guess, the results are different in IE and FF.

  • CSS inclusion - IE fires both events, onload first, then onreadystatechange. FF fires nothing.
  • JS inclusion - IE fires onreadystatechange. FF fires onload. Both will execute the script before firing the event.

Conclusion

1. So there is, after all, a cross-browser way to tell when a JavaScript is actually included and that is to attach two event listeners - onload and onreadystatechange.
2. In IE you have two ways to tell when a CSS is included.

Bookmark and Share

Somewhat related posts

34 Responses to “JS includes - the saga continues…”

  1. phpied.com » Blog Archive » Javascript includes - yet another way of RPC-ing Says:

    […] 2006-10-25 update: The cross-browser way to tell when a script is loaded is here. Post this entry to: » del.icio.us  » Digg  » Furl  » Newsvine  » reddit  » Y! […]

  2. phpied.com » Blog Archive » When is a JavaScript include ready? Says:

    […] 2006-10-25 update: Thanks to the comments, the cross-browser way is here. Post this entry to: » del.icio.us  » Digg  » Furl  » Newsvine  » reddit  » Y! […]

  3. matt Says:

    Does this work in Safari???

  4. Stoyan Says:

    Hi Matt, I don't hava a Mac at hand in order to test. If anyone can test it, that would be great. I have the suspicion that onreadystatechange won't work, but I have hopes for onload.

  5. Jason Says:

    Great ideas on remote scripting, all three articles I saw were very excellent. On the subject of determining when a script has loaded, it seems to me that the reason that you need to know when the script is loaded is to do something, which of course may or may not be the case depending on what you personally are using the scripts for. For myself, I found that a nice, and cross-browser, way of doing this is with a callback. If you place a function in the base script that the dynamically loaded script calls then the function can only be called when the script has been loaded (I think, it seems to work in practice and the testing I did seems to confirm this).

    Base script (local.js):

    > function callback( data ) {
    >
    > // Do something…
    > }

    Dynamically loaded script (remote.js):

    > // Create data, do other functions of script
    >
    > // Make the callback
    > callback( data );

    Hope this adds to your thoughts and maybe your code.

    Regards,
    Jason

  6. choan Says:

    Matt, Stoyan: unfortunately this method doesn't work on Safari (2.0.4).

  7. » Carga dinámica de scripts: una buena aproximación - Scriptia Says:

    […] Stoyan Stefanov ha publicado recientemente en phpied.com un par de notas. Anda buscando (como muchos de nosotros) una buena manera de cargar scripts externos dinámicamente. El truco: crear un elemento script y asignarle un manejador para onreadystatechange (IE) o onload (Firefox). Desafortunadamente, esta aproximación deja fuera a Safari y Opera. Snif. Los enlaces: When is a JavaScript include ready?y JS includes - the saga continues…. Etiquetas en algún lugar, include, carga dinámica […]

  8. Tomek Says:

    Not sure if it's a mistake in the code or some difference between browser's version but js.readyState == 'complete' didn't work for me in IE6 - I used js.readyState == 'loaded' instead.

  9. Chris Says:

    js = document.createElement('script');
    js.onload = myfunc; //firefox only
    js.onreadystatechange = function() { if(script.readyState=='loaded') myfunc(); }; //IE only

    I tested this against many different browsers

    IE6: works!
    IE7: works!
    FireFox: works!
    Safari - does NOT work :(

    Anyone know a solution for Safari?

  10. Neal Says:

    Safari solution :)

    I discovered a method to make this work with Safari. It's a significant change from the above scripts, but seems to do the job in all major browsers. The basic concept is to use AJAX to gather the JS code from the JS file and then append that as the text of the script object.

    The downside to this method is that it will not work cross-domain without some additional work.

    Here's the basic code:
    ====================================

    function getHTTPObject() {
    var req = false;
    // branch for native XMLHttpRequest object
    if(window.XMLHttpRequest) {
    try { req = new XMLHttpRequest(); }
    catch(e) {req = false;}
    // branch for IE/Windows ActiveX version
    }
    else if(window.ActiveXObject) {
    try { req = new ActiveXObject("Msxml2.XMLHTTP"); }
    catch(e) {
    try {req = new ActiveXObject("Microsoft.XMLHTTP");}
    catch(e) {req = false;}
    }
    }
    return req;
    }

    function include_js(file) {
    oHttp = getHTTPObject();
    if (oHttp.readyState != 0) {oHttp.abort();}
    oHttp.open("get", file , true);
    oHttp.onreadystatechange = function () {
    if (oHttp.readyState == 4) {
    var html_doc = document.getElementsByTagName('head')[0];
    var js = document.createElement("script");
    js.type="text/javascript";
    js.text = oHttp.responseText;
    if (!document.all) {
    js.innerHTML = oHttp.responseText;
    }
    html_doc.appendChild(js);
    test1();
    }
    }
    oHttp.send(null);
    }

    function test1() {
    alert(JS File Loaded);
    }

    ============================================

    If you need to work cross domain, we modify the above code to use an ASP page as an intermediary between the client and the server hosting the js file. To do this we need to modify the above code and add an asp file to act as an go between:

    Here's the code:
    ============================================
    function include_js(file) {
    oHttp = getHTTPObject();
    if (oHttp.readyState != 0) {oHttp.abort();}
    // URL to the file that will access the data
    var vURL = "jsload1.asp?file=" + file // file must be complete URL to js file
    oHttp.open("get", vURL , true);
    oHttp.onreadystatechange = function () {
    if (oHttp.readyState == 4) {
    var html_doc = document.getElementsByTagName('head')[0];
    var js = document.createElement("script");
    js.type="text/javascript";
    js.text = oHttp.responseText;
    if (!document.all) {
    js.innerHTML = oHttp.responseText;
    }
    html_doc.appendChild(js);
    test1();
    }
    }
    oHttp.send(null);
    }
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

    This makes use of a file I called jsload1.asp. The code for that page is below. Use this code exactly. Don't add doctype or HTML or other tags as they will be imported along with the javascript causing problems.
    ===================================

  11. Neal Says:

    It appears that the ASP page code didn't display in the last post. I'll post it here again without the ASP script delimiters around it.

    Response.Buffer = True
    Dim objXMLHTTP, xml
    Set xml = Server.CreateObject("Microsoft.XMLHTTP")
    xml.Open "GET", request.QueryString("file"), False
    xml.Send
    Response.Write xml.responseText
    Set xml = Nothing

  12. Stoyan Says:

    Thanks Neal, this is very cool.

    I was only looking at the JS includes as an alternative to AJAX, hence looking for non-AJAX solution, but it doesn't have to be the only application to this technique.

    Thanks again for sharing this!

  13. Phil Says:

    I believe I have found a working method to fire an onload in Safari using both an iframe and a script tag. I agree that it's a slightly messy method (and requires two calls to the server) but it does guarantee that the script will be loaded. Check http://pnomolos.com/article/5/dynamic-include-of-javascript-in-safari to see my method (once you think about it, it makes perfect sense - of course it's not a very clear solution when you're on the attempting-to-solve end of the problem)

  14. Nathan Says:

    Neal: yes your solution works but it removes the advantage of cross domain scripting.

    Phil: nice use of IFrames to solve for safari. I think you got carried away testing for loaded for IE. loaded will pass even if a 404 is returned I donno if I agree with that.

    The issue I have with this function is you don't pass a callback function for the onload event. Also there is no house cleaning. If the script has loaded we can then remove from the DOM.

    I also found this http://slayeroffice.com/archives/?p=172 which talks about Safari having an onload but you have to set it before you attach it to the document. I haven't been able to verify since I don't have a Mac.

    now the function takes two arguments the file and the call back function. the function returns a unique id for the attach script… and this unique id is passed to your call back function. When I have time I will add a timeout out a third parameter for an onerror callback.

    include_js = (function(){

    var uid = 0;

    var remove = function(id){
    var head = document.getElementsByTagName('head')[0];
    head.removeChild( document.getElementById('jsInclude_'+id) );
    };

    return function(file,callback){
    var callback;
    var id = ++uid;
    var head = document.getElementsByTagName('head')[0];
    var js = document.createElement('script');
    js.setAttribute('type','text/javascript');
    js.setAttribute('src',file);
    js.setAttribute('id','jsInclude_'+uid);
    if( document.all )
    js.onreadystatechange = function(){
    if(js.readyState == "complete"){ callback(id);remove(id); }
    };
    else
    js.onload = function(){
    callback(id); remove(id);
    };
    head.appendChild(js);
    return uid;
    };
    })();

  15. phpied.com » Blog Archive » Dynamic SCRIPT and STYLE elements in IE Says:

    […] So you know how to add external scripts and styles, using the DOM, after the page is loaded. And what if you don't have external files, but have some style definitions and some JS code as text and you want it inserted and executed into a page. […]

  16. mysticav Says:

    OOP Solution with listeners support /Cross-Browser:
    http://pastebin.com/881775

    Please Try to improve it. many things can be added to this class.
    Post it back again.

  17. mysticav Says:

    Improved OOP Solution: dynamicFiles.js
    http://pastebin.com/881805

    Yuu can downlaod multiple files and invoke multiple callbacks when all files are downloaded.

    Improvements are most welcome. Many things can be added to this class.
    Post it back again.

  18. Roberto Says:

    You can try this class. Supports multiple js downloads and multiple callbacks.

    http://pastebin.com/881805

  19. jon adams Says:

    i'm interested in your OOP solution but the site you used pastebin.com is broken, can you let me know where i can find your stuff?

  20. brownsca Says:

    Just wanted to mention that if you need to add multiple scripts at the same time, IE does NOT serialize the requests for you. So if script 'b' is supposed to be loaded after script 'a' you have to make sure it happens.

    Here is some code for those who want it:

    initial call:
    new appendScriptSerializer(alisttoadd, 0);

    function appendScriptSerializer(alisttoadd, idx) {
    var self = this;
    this.alisttoadd= alisttoadd;
    this.idx = idx;

    if (this.idx >= alisttoadd.length) {
    // alert("now time to do something else…");
    // call a wrapup function at end of list
    return;
    }
    var appendIt = listtoadd[idx]);
    function appendNextScript() {
    var nextidx = self.idx + 1;
    new appendScriptSerializer(listtoadd, nextidx);
    }
    var tname = appendIt.tagName.toLowerCase();
    if ((tname == "link" && navigator.appVersion.indexOf("MSIE") != -1) || (tname == "script" && appendIt.getAttribute("src") != null)) {
    addNotify(appendIt, appendNextScript);
    …head…appendChild(appendIt);
    } else {
    …head….appendChild(appendIt);
    appendNextScript();
    }

    }

    addNotify(appendThis, appendNextFunction) {
    if (navigator.appVersion.indexOf("MSIE") == -1) {
    appendThis.onload = appendNextFunction;
    } else {
    //alert('adding function to onreadstatechange');
    appendThis.onreadystatechange = function () {
    if (appendThis.readyState == 'complete' || appendThis.readyState == 'loaded') {
    appendThis.onreadystatechange = null;
    appendNextFunction();
    } else {
    // alert('unexpceted state:'+appendThis.readyState);
    }
    }
    }
    }

  21. Sheneyan Says:

    something wrong?

    I try your code in XP sp2 ie6 , but the js.onload does not affect ?

    I alert each state of it,I find that the last state is 'loaded' besides 'complete'

    It's my code:

    if(/(complete|loaded)/.test(js.readyState)){…}

  22. Matthew Says:

    I've found that IE sometimes only progresses to the 'loaded' state, never acheiving 'complete'. This happens intermittently and was the cause of a very nasty and hard-to-track-down bug for us. Most of the time it progresses ok to 'complete' readyState, but once in a while it will only get to 'loaded'.

    So, I'd suggest checking for either.

  23. WebAdictos - Una dosis diaria de web@ » Archivo del Blog » Incluye tus Archivos JS y CSS Cuando los necesites Says:

    […] Bueno pues no se si a alguno de ustedes les ha surgido el problema de que su sistema esta demasiado cargado de inicio de archivos javascript y css que realmente solo lo utilizan ciertos módulos, pues a mi hace unos meses me surgió ese problema e investigando me puse a trabajar en una libreria que cargara los archivos Javascript mediante el DOM simplemente llamando a un método. La libreria fue basada en este artículo de Sentido Web  y en este otro de phpied. El código es el siguiente: var ScriptLoader = { //Método a llamar para cargar el script loadScript: function(file,opt) { […]

  24. links for 2007-05-02 « toonz Says:

    […] phpied.com » Blog Archive » JS includes - the saga continues… (tags: JavaScript) […]

  25. Thomas Urban Says:

    Thanks for this code on handling an included script's onload event. I tried to get a working solution for Opera9.2 and found one not mentioned before: Opera is supporting DOM2 Event Model and thus it supports addEventListener() just like Firefox (tested with FF2 here). For supporting Firefox and Opera the onload-code in example above might be adjusted like this:

    if ( js.addEventListener )
    js.addEventListener( 'load', function() { your code }, false );

    Could you try this with Safari as it is marked to support DOM2 Event Model as well.

  26. phpied.com » Blog Archive » Delay loading your print CSS Says:

    […] Well, in order to increase rendering performance, all stylesheets not absolutely needed to initially render a page should be loaded after the page load, in the background. Once the user has a fast rendered page to interact with, you can load the additional CSS (and JavaScripts for that matter) in the background, using script and style DOM includes. Post this entry to: » del.icio.us  » Digg  » Furl  » Newsvine  » reddit  » Y! […]

  27. All in a days work… Says:

    […] Delay loading your (print) CSS and / or JavaScript So there is, after all, a cross-browser way to tell when a JavaScript is actually included and that is to attach two event listeners - onload and onreadystatechange. In IE you also have two ways to tell when a CSS is included. (tags: JavaScript CSS IE) Share and Enjoy:These icons link to social bookmarking sites where readers can share and discover new web pages. […]

  28. Incluye tus Archivos JS y CSS Cuando los necesites « Neozeratul en la Red Says:

    […] Incluye tus Archivos JS y CSS Cuando los necesites Bueno pues no se si a alguno de ustedes les ha surgido el problema de que su sistema esta demasiado cargado al inicio de archivos javascript y css que realmente solo lo utilizan ciertos módulos, pues a mi hace unos meses me surgió ese problema e investigando me puse a trabajar en una libreria que cargara los archivos Javascript mediante el DOM simplemente llamando a un método. La libreria fue basada en este artículo de Sentido Web y en este otro de phpied. El código es el siguiente: […]

  29. Tejal Says:

    this code works fine for all browser except IE 6..in my case i have different tabs…user can switch between tabs and when user do that i m loading the script dynamically…that works fine for other browser as well as IE 6…but it fails in IE 6 for below scenario…

    I m loading the multiple scripts when one tab is active. now script are loading for this active tab and user switched to another tab..so on active tab changed other javascritp for this current tab are also loaded dynamically..but on readystate='loaded' || readystate=='completed' i try to call the function of loaded script. but i m getting the undefined error and i found that thought i m getting the readystate = 'loaded' or 'complete' my javascript is not loaded..is there any issue with IE 6 loading the scritp…this happend only when my previous requested script are not loaded and i request another script…but how it is possible to complere the request which was made later….

  30. Philip Tellis Says:

    The link to Javascript Includes on the right hand side of your homepage is broken.

  31. Stoyan Says:

    Thanks Philip, fixed now.

  32. Benjamin Says:

    Hello! I have the same issue as Tejal. With IE6, it seems that the script is loaded but not excuted (except if there is an "alert" in the script).
    Once the script loaded, any call to a variable or a function defined in that script will generate a "undefined object" error.

  33. dottwatson Says:

    Hi!!
    I've found a problem whit IE6 and its cache.
    If I make the call on a page opened for first time, the readyState result == 'loaded', and the second time on the same page NOT reloaded, the readyState is == 'complete'.

    i've resolved whit

    jsel.onreadystatechange = function ()
    {
    if (jsel.readyState == 'loaded' || jsel.readyState == 'complete')
    {
    alert('JS onreadystate fired');
    }
    }

    please note that in IE6, the second call to the same object is not really executed!

    My experiment was tested on a php page whit a for cicle from 1 to 10000 and the execution time is aboute more than 3 sec.
    first time i've called this script (whit empty iE6 cache) , the execution time was exactly 3 sec… right! the popup have respected the execution script :)

    but second time it was immediate… cache control?

Leave a Reply