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 the relevant 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 here.
You can see ensure in action from this site.
ensure supports jQuery, Microsoft ASP.NET AJAX, and the Prototype framework. This means you can use it on any HTML, ASP.NET, PHP, JSP page that uses any of the above frameworks.
Background
Websites with rich client side effects (animations, validations, menus, pop-ups) and AJAX websites require large amounts 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 the browser gets sluggish when responding to actions. You sometimes see pull-down menus getting stuck, popups appearing slowly, window scroll being sluggish, and so on.
The solution is not to deliver all possible HTML, JavaScript, and CSS on initial load, instead to deliver them when needed. For example, when the user hovers the mouse on the menu bar, download the 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 the client side validation library, the relevant warning HTML snippets, and the CSS when the user clicks the 'Submit' button. If you have an AJAX site which shows pages on demand, you can load the AJAX library itself only when the 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 size of the initial delivery and thus load the initial page really fast and give the user a fast and smooth browsing experience.
Benefits of ensure
ensure saves you from delivering unnecessary JavaScript, HTML, and CSS upfront, and instead loads them whenever needed, on-demand. JavaScript, HTML and CSS loaded by ensure remain in the browser, and the next time 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 that Some.js is available before executing the code. If SomeJS.js has already been loaded, it executes the function right away. Otherwise, it downloads Some.js, waits until it is properly loaded, and only then it executes the function. Thus, it saves you from delivering 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 users on your default web page. You can fetch the HTML whenever you need it.
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, your code can safely use the UI element from that HTML.
You can mix and match JavaScript, HTML, and CSS all together in one ensure
call. For example:
ensure( { js: "popup.js", html: "popup.html", css: "popup.css" }, function()
{
PopupManager.show();
});
You can also specify multiple JavaScript, HTML, or CSS files to ensure that 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 that will result in a larger JavaScript file than before. In order to save your JavaScript size, you can define short-hands 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 the 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 that 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 frameworks:
- jQuery
- Microsoft ASP.NET AJAX
- Prototype
First comes the definition of the 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 ensureExecutor
. Basically, ensure
creates an instance of ensureExecute
and passes the relevant data, callback, and scope to it. The real work for loading the stuff and calling back the callback is done within ensureExecutor
.
First, ensureExecutor
does some preparation on the parameters and ensures the valid parameters are there. Then, it fires the init
function to initialize the currently available Framework (jQuery/Microsoft 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 functions getJS
and httpGet
which load 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 JavaScript has 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 the loadCSS
or loadHTML
function gets fired.
The loadCSS
function is rather painless. The only gotcha is, in Internet Explorer 6, 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 downloads the HTML and injects it inside the 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 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 the XMLHTTP
call to download the script and then execute it. However, this means you cannot ensure a script from an external domain on Safari as XMLHTTP
calls work only with the current domain.
Here you can see three ways to download a 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 the downloaded script at a 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 a window object's scope. Nope, does not work. The only fast, cross browser 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 the <head>
node and then passes the script text to it.
Real Life Examples
The test application shows you some common uses 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 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 get 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 fade-in 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 how you can download multiple JavaScript, HTML, and CSS, all in one shot.
Download Code
Download latest source code from CodePlex.
Conclusion
Now you can ensure that the 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.