As we are writing more and more complex JavaScript applications we run into issues that have hitherto (god I love that word) not been an issue. The first decision we have to make is what to do when planning our app: one big massive JS file or a lot of smaller, specialised files separated by task.
Personally, I tend to favour the latter, mainly because it allows you to work on components in parallel with other developers without lots of clashes in your version control. It also means that your application will be more lightweight as you only include components on demand.
Starting with a global object
This is why it is a good plan to start your app with one single object that also becomes the namespace for the whole application, say for example myAwesomeApp:
var myAwesomeApp = {};
You can nest any necessary components into this one and also make sure that you check for dependencies like DOM support right up front.
Adding the components
The other thing to add to this main object is a components object, which defines all the components that are there and their file names.
var myAwesomeApp = {
components :{
formcheck:{
url:'formcheck.js',
loaded:false
},
dynamicnav:{
url:'dynamicnav.js',
loaded:false
},
gallery:{
url:'gallery.js',
loaded:false
},
lightbox:{
url:'lightbox.js',
loaded:false
}
}
};
Technically you can also omit the loaded properties, but it is cleaner this way. The next thing to add is an addComponent function that can load your components on demand by adding new SCRIPT elements to the head of the documents when they are needed.
var myAwesomeApp = {
components :{
formcheck:{
url:'formcheck.js',
loaded:false
},
dynamicnav:{
url:'dynamicnav.js',
loaded:false
},
gallery:{
url:'gallery.js',
loaded:false
},
lightbox:{
url:'lightbox.js',
loaded:false
}
},
addComponent:function(component){
var c = this.components[component];
if(c && c.loaded === false){
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('src',c.url);
document.getElementsByTagName('head')[0].appendChild(s);
}
}
};
This allows you to add new components on the fly when they are not defined:
if(!myAwesomeApp.components.gallery.loaded){
myAwesomeApp.addComponent('gallery');
};
Verifying that components have been loaded
However, this is not safe as the file might not be available. To make the dynamic adding of components safer each of the components should have a callback at the end of them that notifies the main object that they indeed have been loaded:
var myAwesomeApp = {
components :{
formcheck:{
url:'formcheck.js',
loaded:false
},
dynamicnav:{
url:'dynamicnav.js',
loaded:false
},
gallery:{
url:'gallery.js',
loaded:false
},
lightbox:{
url:'lightbox.js',
loaded:false
}
},
addComponent:function(component){
var c = this.components[component];
if(c && c.loaded === false){
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('src',c.url);
document.getElementsByTagName('head')[0].appendChild(s);
}
},
componentAvailable:function(component){
this.components[component].loaded = true;
}
}
For example the gallery.js file should call this notification as a last line:
myAwesomeApp.gallery = function(){
// [... other code ...]
}();
myAwesomeApp.componentAvailable('gallery');
Telling the implementers when components are available
The last thing to add (actually as a courtesy measure for debugging and implementers) is to offer a listener function that gets notified when the component has been loaded:
var myAwesomeApp = {
components :{
formcheck:{
url:'formcheck.js',
loaded:false
},
dynamicnav:{
url:'dynamicnav.js',
loaded:false
},
gallery:{
url:'gallery.js',
loaded:false
},
lightbox:{
url:'lightbox.js',
loaded:false
}
},
addComponent:function(component){
var c = this.components[component];
if(c && c.loaded === false){
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('src',c.url);
document.getElementsByTagName('head')[0].appendChild(s);
}
},
componentAvailable:function(component){
this.components[component].loaded = true;
if(this.listener){
this.listener(component);
};
}
};
This allows you to write a main listener function that acts when certain components have been loaded, for example:
myAwesomeApp.listener = function(component){
if(component === 'gallery'){
showGallery();
}
};
Extending with other components
As the main object is public, other developers can extend the components object with own components and use the listener function to load dependent components. Say you have a bespoke component with data and labels in extra files:
myAwesomeApp.listener = function(component){
if(component === 'bespokecomponent'){
myAwesomeApp.addComponent('bespokelabels');
};
if(component === 'bespokelabels'){
myAwesomeApp.addComponent('bespokedata');
};
if(component === 'bespokedata'){
myAwesomeApp,bespokecomponent.init();
};
};
myAwesomeApp.components.bespokecomponent = {
url:'bespoke.js',
loaded:false
};
myAwesomeApp.components.bespokelabels = {
url:'bespokelabels.js',
loaded:false
};
myAwesomeApp.components.bespokedata = {
url:'bespokedata.js',
loaded:false
};
myAwesomeApp.addComponent('bespokecomponent');
Following this practice you can write pretty complex apps and still have full control over what is available when. You can also extend this to allow for CSS files to be added on demand.
Influences
If you like this idea and wondered if someone already uses it, take a look at the Yahoo! User Interface library, and especially at the YAHOO_config option of the global YAHOO.js object.



Comments
Comments are ordered by helpfulness, as indicated by you. Help us pick out the gems and discourage asshattery by voting on notable comments.
Got something to add? You can leave a comment below.
20/12/2007
Christian,
You say that this technique can be applied to CSS files as well, but I’m curious about how you would know if the CSS file actually loaded? That is, how would the myAwesomeApp.componentAvailable() get called for CSS file(s)?
Vote Helpful or Unhelpful
18/12/2007
Good article. I would like to add for people who want to use this to also take a look at: unobtrusive javascript which will help you to write good code for the dynamically included javascript that will ‘play nice’ with other js code on the page.
It’s written by someone named: Christian Heilmann. The name sound kinda familiar, don’t know why though :o).
Vote Helpful or Unhelpful
19/12/2007
on the subject of lazy loading js, you might find this interesting: <a href=“http://tech.groups.yahoo.com/group/ydn-javascript/message/10686” target=”_blank”>Using yui_2.2.0a with JSAN</a> or this <a href=“http://tech.groups.yahoo.com/group/ydn-javascript/message/15052” target=”_blank”>Crockford thoughts on JavaScript package systems?</a>
Vote Helpful or Unhelpful
20/12/2007
This is a nice solution, but it still has disadvantages. For one, components loaded through addComponent will not be generally available until the current script block has exited and the next has begun. What this means is that if we want to write some component ‘A’ which depends on some other component ‘B’, then all of ‘A’ must be placed inside a listener function that checks for the availability of ‘B’ (and what’s with this implementation that doesn’t allow more than one single listener registered for the entire component hierarchy? Something like the ObserverPattern should have been used).
Worse, if ‘A’ depends on both ‘B’, ‘C’ and ‘D’, then the listener function must painfully remember which of these components are ready, and only execute ‘A’ when all of them have been loaded.
Result: very nasty and voluminous dependency management code.
Another problem would be when some of these components are not specific to myAwesomeApp, but of a more general nature. Then it just doesn’t work at all.
Personally I prefer a more general approach. Traces of it can be seen in the ObserverPattern.js linked above. It contains a line reading “Script.require(‘Prototype’);”. Obviously, what this means, is that this “package” (or component if you will) does not work without first loading something referenced as “Prototype”. “Script” (a generic dependency management package) resolves this dependency by looking up the “Prototype” key in an application specific alias table (because the same components are available from different URI’s in different applications), and then immediately makes this dependency available (an option is included for loading in the same manner as above, but rarely used). The result is one line of code per dependency – that doesn’t change even when components are moved around.
Full source code is available.
Vote Helpful or Unhelpful
21/12/2007
I am surprised that you recommend many smaller js files as opposed to one larger. This goes against the principles of YSlow, which I thought voiced the opinion of all YAHOO front-end engineers. I guess you don’t all agree, nothing wrong with that….
Personally I prefer one larger file, minified, gzipped, and cached, this will be a better experience for the user.
Vote Helpful or Unhelpful
Impress us