Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

ensure - Ensure JavaScripts/HTML/CSS are loaded on-demand when needed

4.97/5 (22 votes)
9 Jun 2008CPOL8 min read 2   612  
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 relevant JavaScript and HTML snippets are already in the browser DOM before executing your code that uses them.

Ensure.png

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:

JavaScript
ensure( { js: "Some.js" }, function()
{
    SomeJS(); // The function SomeJS is available in Some.js only
});

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:

JavaScript
ensure( { js: "Some.js" }, function()
{
    SomeJS(); // The function SomeJS is available in Some.js only
}); 

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.

JavaScript
ensure( {html: "Popup.html"}, function()
{
    // The element "Popup" is available only in Popup.html
    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:

JavaScript
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:

JavaScript
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:

JavaScript
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".

JavaScript
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.

JavaScript
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:

JavaScript
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:

JavaScript
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");
        
    // There's a test criteria which when false, 
    //the associated components must be loaded. But if true, 
    // no need to load the components
    if( typeof data.test != "undefined" )
    {
        var test = function() { return data.test };
        
        if( typeof data.test == "string" )
        {
            test = function() 
            { 
                // If there's no such Javascript variable and there's 
        // no such DOM element with ID then
                // the test fails. If any exists, then test succeeds
                return !(eval( "typeof " + data.test ) == "undefined" 
                    && document.getElementById(data.test) == null); 
            }
        }    
        else if( typeof data.test == "function" )      
        {
            test = data.test;
        }
        
        // Now we have test prepared, time to execute the test 
    // and see if it returns null, undefined or false in any 
        // scenario. If it does, then load the specified javascript/html/css    
        if( test() === false || typeof test() == "undefined" || test() == null ) 
            new ensureExecutor(data, callback, scope);
        // Test succeeded! Just fire the callback
        else
            callback();
    }
    else
    {
        // No test specified. So, load necessary javascript/html/css 
        // and execute the callback
        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.

JavaScript
window.ensureExecutor.prototype = {
    init : function()
    {
        // Fetch Javascript using Framework specific library
        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.

JavaScript
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.

JavaScript
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);
                            })
            });
        }
    });
    
    // wait until all the external scripts are downloaded
    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.

JavaScript
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) )
        {
            // Do nothing
        }
        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.

JavaScript
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);
              })
      });
    }            
  });
  
  // wait until all the external scripts are downloaded
  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:

JavaScript
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:

JavaScript
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:

JavaScript
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 );
    //head.removeChild( 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:

JavaScript
<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.

HTML
<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.

JavaScript
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)