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,
onloadfirst, thenonreadystatechange. FF fires nothing. - JS inclusion - IE fires
onreadystatechange. FF firesonload. 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.
October 25th, 2006 at 9:49 pm
[…] 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! […]
October 25th, 2006 at 9:50 pm
[…] 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! […]
November 3rd, 2006 at 6:46 am
Does this work in Safari???
November 3rd, 2006 at 9:34 am
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
onreadystatechangewon't work, but I have hopes foronload.November 5th, 2006 at 5:28 pm
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
November 11th, 2006 at 12:10 pm
Matt, Stoyan: unfortunately this method doesn't work on Safari (2.0.4).
November 11th, 2006 at 12:40 pm
[…] 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 […]
December 12th, 2006 at 8:41 am
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.
December 29th, 2006 at 2:09 pm
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?
December 29th, 2006 at 5:30 pm
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.
===================================
December 29th, 2006 at 5:34 pm
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
January 2nd, 2007 at 10:44 am
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!
January 8th, 2007 at 12:42 am
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)
January 19th, 2007 at 2:55 pm
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;
};
})();
January 27th, 2007 at 2:41 pm
[…] 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. […]
February 15th, 2007 at 7:59 pm
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.
February 15th, 2007 at 9:18 pm
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.
February 17th, 2007 at 5:50 pm
You can try this class. Supports multiple js downloads and multiple callbacks.
http://pastebin.com/881805
March 12th, 2007 at 7:00 pm
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?
March 18th, 2007 at 9:09 pm
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);
}
}
}
}
March 22nd, 2007 at 5:45 am
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)){…}
April 3rd, 2007 at 7:25 am
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.
April 13th, 2007 at 6:13 pm
[…] 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) { […]
May 2nd, 2007 at 6:42 pm
[…] phpied.com » Blog Archive » JS includes - the saga continues… (tags: JavaScript) […]
May 20th, 2007 at 5:20 pm
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.
June 17th, 2007 at 10:42 pm
[…] 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! […]
June 22nd, 2007 at 6:58 pm
[…] 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. […]
August 15th, 2007 at 12:08 pm
[…] 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: […]
March 3rd, 2008 at 8:22 am
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….
April 5th, 2008 at 9:46 pm
The link to Javascript Includes on the right hand side of your homepage is broken.
April 5th, 2008 at 11:23 pm
Thanks Philip, fixed now.
April 10th, 2008 at 5:20 am
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.
April 22nd, 2008 at 6:41 pm
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?