When is a JavaScript include ready?
This is a follow up to my article (the most popular on my blog based on the comments) about the JavaScript includes, the technique to include new .js files after the page is loaded, by using DOM to create a new script tag. The problem that is discussed in the comments is how to find out when/if the new script was actually downloaded. Here's a solution (IE only!).
Today I came accross an article on MSDN, written back in 1998 where they talk about readyState property of an inline JavaScript. So I decided to try it in conjunction with my JS includes. It worked! The solution is IE-only, but probably there is something similar for Firefox. Please post a comment if you know one.
The idea is that after a new DOM element (a script tag) is created, you can have access to the readyState property of the element. If it says "complete", then the new script is included and it's OK to call functions from it. If you want to "listen" when the script download will be completed, you can attach an listener to the onreadystatechange event, just like with XMLHttpRequests.
Here's an example:
var js; function include_js(file) { var html_doc = document.getElementsByTagName('head').item(0); js = document.createElement('script'); js.setAttribute('type', 'text/javascript'); js.setAttribute('src', file); html_doc.appendChild(js); // alert state change js.onreadystatechange = function () { alert(js.readyState); if (js.readyState == 'complete') { // safe to call a function // found in the new script imready(); } } return false; }
This also works if you decide to include new CSS files on the fly. If you want to know in your javascript when the CSS is downloaded, you can do the same check.
Here's a demo that includes a CSS and alert()s onreadystatechange and also includes a JS, alerts the state change and when "complete", calls a function from the newly inlcluded script.
This solution to the problem "When is my include loaded?" is perfect, if you ask me, very clean and simple. The problem is it's IE-only, but the good news is that FF won't give you an error, it will just work as if the extra code was not there, simply because FF won't fire an onreadystatechange event.
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!
October 23rd, 2006 at 10:19 am
This untested but I dont see any reason why it wouldnt work, and should work across all browsers.
Right at the end of the external js file set a variable like:
var externalScriptLoaded = true;
Then in your main code you could use:
function checkScriptLoaded() {
if(!externalScriptLoaded) {
setTimout("checkScriptLoaded", 500);
} else {
// script loaded and ready to use
}
}
If you have multiple external files you want to check just make sure each has a unique variable name being set and check them accordingly.
This polling every half-second isnt perfect but it will work.
Your other option would be to create a function/method in your main script which inits the actions to perform when the external script is loaded.
Then as the very last thing in your external script you could just call that function/method
externalScripts.myScriptLoaded();
As this call would be at the end of your file it wont be run until the rest of the file ahead of it has been downloaded.
Once again this method is untested but am 99% sure will work and will be cross browser.
cheers
October 23rd, 2006 at 1:45 pm
Thanks Aaron, both your methods should work. I was just trying to find a way to test if the script is loaded without any extra code in the new script. One line of code like in your suggestion is not a big deal, but what about when you don't have access to modify the script you're including?
October 23rd, 2006 at 1:52 pm
[…] 2006-10-23 update: In IE, I found a way to tell when the new script is done loading - described here. […]
October 23rd, 2006 at 4:32 pm
Stoyan: Yeah I know there could be times when the methods I mentioned wont be an option, but then there will be times when you'll need to support more than 1 browser
Depending upon the make up of the external file you are calling you *might* not need to alter it at all. As long as there is a variable/etc being set towards the end of the file you can check for it.
Same as when you use Object detection to test for browser support
if(document.getElementById) { doDomStuff(); }
I also had a thought after having another look at your code, I know if you set an images src attribute as a js file certain browsers will download and execute it like any other external js file (I cant quite remember which at the moment)
So it might be possible to use the Image object's onload event?
If I can escape my better half's calls to help around the flat tonight I might get a chance to test this but am thinking something along the lines of….
function loadExternal(url) {
var loader = new Image();
loader.onLoad = externalLoaded;
loader.src = url;
}
function externalLoaded() {
// do stuff
}
October 24th, 2006 at 2:01 pm
Using the load event on the SCRIPT object works in Firefox and Opera (and most likely in Safari, too).
October 24th, 2006 at 2:59 pm
Thanks, Björn, wicked! I'll try it out!
November 11th, 2006 at 12:41 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 […]
November 24th, 2006 at 9:13 pm
Hi, I just created a dynamic include script using modified code from this blog. It works by creating an array of files that are included, and also a list of files *the have loaded.*
After calling all the necessary includes, the user calls "waitToLoadFiles(postLoadFunc)" which will periodically check to see if the list of loaded files matches the list of included files. Once the list matches, "postLoadFunc" is executed.
This script is tested in FF1.5.0.8 and works great!! Fails to work in IE5.1, though, and I can't understand why. It seems to load some files, but then it stops. I'm thinking it has something to do with the include_once function being called again within included files. If anyone can figure it out, let me know!
velocityidp at gmail dot com
var js; var html_doc = document.getElementsByTagName('head')[0]; var loaded_files = new Array(); function include_js(js_file) { js = document.createElement('script'); js.setAttribute('type', 'text/javascript'); js.setAttribute('src', js_file); js.file = js_file; html_doc.appendChild(js); js.onreadystatechange = function () { if(this.readyState == 'complete') loaded_files[loaded_files.length] = this.file; } js.onload = function () { loaded_files[loaded_files.length] = this.file; } return false; } var css; function include_css(css_file) { css = document.createElement('link'); css.setAttribute('rel', 'stylesheet'); css.setAttribute('type', 'text/css'); css.setAttribute('href', css_file); css.file = css_file; html_doc.appendChild(css); // alert state change css.onreadystatechange = function () { if (this.readyState == 'complete') loaded_files[loaded_files.length] = this.file; } css.onload = function () { loaded_files[loaded_files.length] = this.file; } return false; } var included_files = new Array(); function include_once(file, type) { if (!in_array(file, included_files)) { included_files[included_files.length] = file; switch(type){ case 'js': include_js(file); break; case 'css': include_css(file); default: include_js(file); break; } } } function in_array(needle, haystack) { for (var i = 0; i < haystack.length; i++) { if (haystack[i] == needle) { return true; } } return false; } var granularity = 1000; //ms function waitToLoadFiles(postLoadFunc){ if(areAllFilesLoaded()) setTimeout(postLoadFunc+"()", 1); else{ alert("Not loaded "+loaded_files.length+"/"+included_files.length+"nn"+loaded_files.toString ()); setTimeout("waitToLoadFiles('"+postLoadFunc+"')", granularity); } } function areAllFilesLoaded(){ if(included_files.length == loaded_files.length) return true; else return false; }January 5th, 2007 at 12:06 am
The problem is that Safari does not work with any of the load event handlers that I have tried. I am attempting to write a script that will load each required JS file in succession and call any code that relies on these files once they are loaded, but Safari will not load any but the first one as I am relying on the load event handlers. Really frustrating, it is.
March 12th, 2007 at 4:59 am
what when a script contains already parameters
like myscript.js.php?id=5
as previous i would make myscript.js.php?id=5?r=465465465
and the server receives id="5?r=465465465"
instead of id=5
notice the:
(script_filename.indexOf("?")?"&":"?")
simpler:
if(script_filename.indexOf("?"))
script_filename +="&"
else
script_filename +="?"
a trick lernt from yahoo UI javascript
when appending p-arameters to a url
function include_rpc(script_filename) {
script_filename += (script_filename.indexOf("?")?"&":"?") + Math.random(0, 1000) + '=' + Math.random(0, 1000);
var html_doc = document.getElementsByTagName('head').item(0);
var js = document.createElement('script');
js.setAttribute('language', 'javascript');
js.setAttribute('type', 'text/javascript');
js.setAttribute('src', script_filename);
html_doc.appendChild(js);
return false;
}