
Introduction
Ensure is a tiny javascript library that provides a handy function ensure which allows you to load Javascript, HTML, CSS on-demand and then execute your code. Ensure ensures that relevent Javascript and HTML snippets are already in the browser DOM before executing your code that uses them.
For example,
ensure( { js: "Some.js" }, function()
{
SomeJS();
});
Download the latest code from: www.codeplex.com/ensure
You can see ensure at action from this site: http://labs.dropthings.com/ensure
Ensure supports jQuery, Microsoft ASP.NET AJAX and Prototype framework. This means you can use it on any html, ASP.NET, PHP, JSP page that uses any of the above framework.
Background
Websites with rich client side effects (animations, validations, menus, popups) and AJAX websites require large amount of Javascript, HTML and CSS to be delivered to the browser on the same web page. Thus the initial loading time of a rich web page increases significantly as it takes quite some time to download the necessary components. Moreover, delivering all possible components upfront makes the page heavy and browser gets sluggish responding to actions. You sometimes see pull-down menus getting stuck, popups appearing slowly, window scroll feels sluggish and so on.
The solution is not to deliver all possible HTML, Javascript and CSS on initial load instead deliver them when needed. For example, when user hovers the mouse on menu bar, download necessary Javascript and CSS for the pull-down menu effect as well as the menu html that appears inside the pull-down. Similarly, if you have client side validations, deliver client side validation library, relevent warning HTML snippets and CSS when user clicks the 'submit' button. If you have a AJAX site which shows pages on demand, you can load the AJAX library itself only when user does the action that results in an AJAX call. Thus by breaking a complex page full of HTML, CSS and Javascript into smaller parts, you can significantly lower down the site of the initial delivery and thus load the initial page really fast and give user a fast smooth browsing experience.
Benefits of ensure
ensure saves you from delivering unnecessary javascript, html and
CSS upfront instead load them whenever needed, on-demand. Javascripts, html and
CSS loaded by ensure remain in the browser and next time when
ensure is called with the same Javascript, CSS or HTML, it does not
reload them and thus saves from repeated downloads.
For example, you can use ensure to download Javascript on
demand:
ensure( { js: "Some.js" }, function()
{
SomeJS();
});
The above code ensures Some.js is available before executing the code. If the
SomeJS.js has already been loaded, it executes the function write away.
Otherwise it downloads Some.js, waits until it is properly loaded and only then
it executes the function. Thus it saves you from deliverying Some.js upfront
when you only need it upon some user action.
Similarly you can wait for some HTML fragment to be available, say a popup
dialog box. There's no need for you to deliver HTML for all possible popup boxes
that you will ever show to user on your default web page. You can fetch the HTML
whenever you need them.
ensure( {html: "Popup.html"}, function()
{
document.getElementById("Popup").style.display = "";
});
The above code downloads the html from "Popup.html" and adds it into the body
of the document and then fires the function. So, you code can safely use the UI
element from that html.
You can mix match Javascript, html and CSS altogether in one
ensure call. For example,
ensure( { js: "popup.js", html: "popup.html", css: "popup.css" }, function()
{
PopupManager.show();
});
You can also specify multiple Javascripts, html or CSS files to ensure all of
them are made available before executing the code:
ensure( { js: ["blockUI.js","popup.js"], html: ["popup.html", "blockUI.html"],
css: ["blockUI.css", "popup.css"] }, function()
{
BlockUI.show();
PopupManager.show();
});
You might think you are going to end up writing a lot of ensure
code all over your Javascript code and result in a larger Javascript file than
before. In order to save you javascript size, you can define shorthands for
commonly used files:
var JQUERY = { js: "jquery.js" };
var POPUP = { js: ["blockUI.js","popup.js"], html: ["popup.html", "blockUI.html"],
css: ["blockUI.css", "popup.css"] };
...
...
ensure( JQUERY, POPUP, function() {
$("DeleteConfirmPopupDIV").show();
});
...
...
ensure( POPUP, function()
{
$("SaveConfirmationDIV").show();
);
While loading html, you can specify a container element where ensure can
inject the loaded HTML. For example, you can say load HtmlSnippet.html and then
inject the content inside a DIV named "exampleDiv"
ensure( { html: ["popup.html", "blockUI.html"], parent: "exampleDiv"}, function(){});
You can also specify Javascript and CSS that will be loaded along with the
html.
Ensure has a test feature where you can check if a particular Javascript class or some UI element is already available. If it is available, it does not download the specified components and executes your code immediately. If not, it downloads them and then executes your code. This is handy when you are trying to use some utility function or some UI element and you want to ensure it is already there.
ensure( {test:"Sys", js:"MicrosoftAjax.js"}, function(){ Sys.Application.init(); });The above example checks if Microsoft AJAX library's Sys class is already there. It will be there if Microsoft AJAX library was already loaded. If it's not there, it loads the library and then calls the code.
Similarly you can ensure some UI element is already there:
ensure( {test:"PopupDIV", js:"Popup.js", html:"popup.html"},
function()
{
document.getElementById("PopupDIV").style.display = "block";
}); This ensures if an HTML element with ID PopupDIV is already there. If not, it downloads the relevant javascript/html and then executes your code.
How it works
The library has only one Javascript file - ensure.js. However, it requires any of the following Javascript framework:
- jQuery
- Microsoft ASP.NET AJAX
- Prototype
First comes the definition of ensure function:
window.ensure = function( data, callback, scope )
{
if( typeof jQuery == "undefined" && typeof Sys == "undefined"
&& typeof Prototype == "undefined" )
return alert("jQuery, Microsoft ASP.NET AJAX or
Prototype library not found. One must be present for ensure to work");
if( typeof data.test != "undefined" )
{
var test = function() { return data.test };
if( typeof data.test == "string" )
{
test = function()
{
return !(eval( "typeof " + data.test ) == "undefined"
&& document.getElementById(data.test) == null);
}
}
else if( typeof data.test == "function" )
{
test = data.test;
}
if( test() === false || typeof test() == "undefined" || test() == null )
new ensureExecutor(data, callback, scope);
else
callback();
}
else
{
new ensureExecutor(data, callback, scope);
}
}
The real work is, however, done in the ensureExecutor. Basically ensure creates one instance of ensureExecute and passes relevant data, callback and scope to it. The real work for loading stuffs and calling back the callback is done within ensureExecutor.
First ensureExecutor does some preparation on the parameters and ensures valid parameters are there. Then it fires the init function to initialize currently available Framework (jQuery/MS AJAX/Prototype) for some common AJAX operation. Then it fires the load function to load the necessary components and fire the callback.
window.ensureExecutor.prototype = {
init : function()
{
if( typeof jQuery != "undefined" )
{
this.getJS = HttpLibrary.loadJavascript_jQuery;
this.httpGet = HttpLibrary.httpGet_jQuery;
}
else if( typeof Prototype != "undefined" )
{
this.getJS = HttpLibrary.loadJavascript_Prototype;
this.httpGet = HttpLibrary.httpGet_Prototype;
}
else if( typeof Sys != "undefined" )
{
this.getJS = HttpLibrary.loadJavascript_MSAJAX;
this.httpGet = HttpLibrary.httpGet_MSAJAX;
}
else
{
throw "jQuery, Prototype or MS AJAX framework not found";
}
},
Here, the init function checks what framework is currently loaded and according to that initializes two function getJS and httpGet which loads an external script and external HTML respectively.
load : function()
{
this.loadJavascripts( this.delegate( function() {
this.loadCSS( this.delegate( function() {
this.loadHtml( this.delegate( function() {
this.callback()
} ) )
} ) )
} ) );
},The load function calls loadJavascripts, loadCSS and loadHtml sequentially. This ensures the HTML is loaded only after the Javascripts have been successfully loaded and CSS loading is either done or already started.
loadJavascripts is the tricky function. It loads an external script by either creating a <script> tag or using XMLHTTP to download an external script. Safari requires XMLHTTP because there's no way to know when a <script> tag has successfully downloaded.
loadJavascripts : function(complete)
{
var scriptsToLoad = this.data.js.length;
if( 0 === scriptsToLoad ) return complete();
this.forEach(this.data.js, function(href)
{
if( HttpLibrary.isUrlLoaded(href) ||
this.isTagLoaded('script', 'src', href) )
{
scriptsToLoad --;
}
else
{
this.getJS({
url: href,
success: this.delegate(function(content)
{
scriptsToLoad --;
HttpLibrary.registerUrl(href);
}),
error: this.delegate(function(msg)
{
scriptsToLoad --;
if(typeof this.data.error == "function")
this.data.error(href, msg);
})
});
}
});
this.until({
test: function() { return scriptsToLoad === 0; },
delay: 50,
callback: this.delegate(function()
{
complete();
})
});
}, The idea is to issue script download and wait until all scripts are downloaded. When done, it fires the complete callback and then loadCSS or loadHTML function gets fired.
loadCSS function is rather painless. The only gotcha is, in IE6, you have to add a <link> tag only when the code is executing in the window object's context. My other article at CodeProject about UFrame explains this problem in detail.
loadCSS : function(complete)
{
if( 0 === this.data.css.length ) return complete();
var head = HttpLibrary.getHead();
this.forEach(this.data.css, function(href)
{
if( HttpLibrary.isUrlLoaded(href) || this.isTagLoaded('link', 'href', href) )
{
}
else
{
var self = this;
try
{
(function(href, head)
{
var link = document.createElement('link');
link.setAttribute("href", href);
link.setAttribute("rel", "Stylesheet");
link.setAttribute("type", "text/css");
head.appendChild(link);
HttpLibrary.registerUrl(href);
}).apply(window, [href, head]);
}
catch(e)
{
if(typeof self.data.error == "function")
self.data.error(href, e.message);
}
}
});
complete();
}
Finally the loadHTML function that downloads the HTML and injects it inside document.body or any parent container element that you have specified.
loadHtml : function(complete)
{
var htmlToDownload = this.data.html.length;
if( 0 === htmlToDownload ) return complete();
this.forEach(this.data.html, function(href)
{
if( HttpLibrary.isUrlLoaded(href) )
{
htmlToDownload --;
}
else
{
this.httpGet({
url: href,
success: this.delegate(function(content)
{
htmlToDownload --;
HttpLibrary.registerUrl(href);
var parent = (this.data.parent ||
document.body.appendChild(
document.createElement("div")));
if( typeof parent == "string" )
parent = document.getElementById(parent);
parent.innerHTML = content;
}),
error: this.delegate(function(msg)
{
htmlToDownload --;
if(typeof this.data.error == "function")
this.data.error(href, msg);
})
});
}
});
this.until({
test: function() { return htmlToDownload === 0; },
delay: 50,
callback: this.delegate(function()
{
complete();
})
});
That's it.
No wait, there's this HttpLibrary class that does the most complicated work - loading and executing Javascript and making AJAX calls.
Here's how you can load an external script using a <SCRIPT> tag and know when the script has loaded in a cross browser fashion:
createScriptTag : function(url, success, error)
{
var scriptTag = document.createElement("script");
scriptTag.setAttribute("type", "text/javascript");
scriptTag.setAttribute("src", url);
scriptTag.onload = scriptTag.onreadystatechange = function()
{
if ( (!this.readyState || this.readyState == "loaded"
|| this.readyState == "complete") ) {
success();
}
};
scriptTag.onerror = function()
{
error(data.url + " failed to load");
};
var head = HttpLibrary.getHead();
head.appendChild(scriptTag);
},
Looks simple, but there's much sweat and blood has gone into this to make it work perfectly in all popular browsers. But still Safari 2 does not support the onload or onreadystatechange events. So, for Safari, the trick is to make XMLHTTP call to download the script and then execute it. However, this means you cannot ensure a script from external domain on Safari as XMLHTTP calls works only with the current domain.
Here you see three ways to download script and execute it:
loadJavascript_jQuery : function(data)
{
if( HttpLibrary.browser.safari )
{
return jQuery.ajax({
type: "GET",
url: data.url,
data: null,
success: function(content)
{
HttpLibrary.globalEval(content);
data.success();
},
error: function(xml, status, e)
{
if( xml && xml.responseText )
data.error(xml.responseText);
else
data.error(url +'\n' + e.message);
},
dataType: "html"
});
}
else
{
HttpLibrary.createScriptTag(data.url, data.success, data.error);
}
},
loadJavascript_MSAJAX : function(data)
{
if( HttpLibrary.browser.safari )
{
var params =
{
url: data.url,
success: function(content)
{
HttpLibrary.globalEval(content);
data.success(content);
},
error : data.error
};
HttpLibrary.httpGet_MSAJAX(params);
}
else
{
HttpLibrary.createScriptTag(data.url, data.success, data.error);
}
},
loadJavascript_Prototype : function(data)
{
if( HttpLibrary.browser.safari )
{
var params =
{
url: data.url,
success: function(content)
{
HttpLibrary.globalEval(content);
data.success(content);
},
error : data.error
};
HttpLibrary.httpGet_Prototype(params);
}
else
{
HttpLibrary.createScriptTag(data.url, data.success, data.error);
}
},
One cool trick is to execute downloaded script at global context. You might think it's easy to do using the eval function. But it does not work. eval executes the call only within the current scope. So, you might think, you can call eval on window object's scope. Nope, does not work. The only cross browser fastest solution is this approach:
globalEval : function(data)
{
var script = document.createElement("script");
script.type = "text/javascript";
if ( HttpLibrary.browser.msie )
script.text = data;
else
script.appendChild( document.createTextNode( data ) );
var head = HttpLibrary.getHead();
head.appendChild( script );
}
It creates a <script> tag inside <head> node and then passes the script text to it.
Real life examples
The test application shows you some common use of ensure. For example, when a button is clicked, you need to call some javascript function. That Javascript function is (or can easily be) in an external javascript file. Here's how you do it:
<input id="example1button" type="button" value="Click me"
onclick="
this.value='Loading...';
ensure({js:'Components/SomeJS.js'}, function(){
SomeJS();
this.value='Click me';
}, this)" /> So, you see SomeJS is available in the SomeJS.js.
The next example shows how you can load some html snippet on-demand inside a DIV.
<input id="example2button" type="button" value="Load Html, CSS on-demand"
onclick="
this.value='Loading...';
ensure({
html:'Components/HtmlSnippet.htm',
css:'Components/HtmlSnippet.css',
parent:'resultDiv'},
function(){
document.getElementById('clickMe').onclick = function()
{
alert('Clicked');
};
this.value='Load Html, CSS on-demand'
}, this)" />When the button is clicked, HtmlSnippet.html and HtmlSnippet.css gets loaded. The content in HtmlSnippet.html is injected inside a DIV named resultDIV. When the content is available and successfully injected, the callback function is fired where the code tries to hook on a button that has come from HtmlSnippet.html.
You may have skipped another cool aspect, the whole callback function is fired on the context of the <input> button. The third parameter to ensure ensures this. You see, I have passed this as the scope, which is the button itself. Thus when the callback fires, you can still use this to access the button.
The third example in the test application shows how several HTML, Javascript and CSS are loaded to provide two UI effects - a background fadein and a popup dialog box.
function showPopup()
{
ensure({
js: 'Components/BlockUI.js',
html: ['Components/BlockUI.html','Components/Popup.aspx'],
css: 'Components/Popup.css'
},
function()
{
BlockUI.show();
var popup = document.getElementById('Popup');
if( null == popup ) alert('Popup is not loaded!');
else popup.style.display = 'block';
document.getElementById('example3button').value = "Show me the UI";
});
}
This code shows you can download multiple Javascript, HTML and CSS all in one shot.
Download Code
Download latest source code of from CodePlex: www.codeplex.com/ensure
Conclusion
Now you can ensure necessary Javascript, HTML, CSS are available before using them. Ensure you use ensure throughout your web application to ensure fast download time and yet ensure UI features are not compromised and thus ensure richer user experience ensuring fast page loading.