5,693,936 members and growing! (16,546 online)
Email Password   helpLost your password?
Web Development » Ajax and Atlas » Libraries     Intermediate License: The Code Project Open License (CPOL)

ensure - Ensure Javascripts/HTML/CSS loaded on-demand when needed

By Omar Al Zabir

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.
Javascript, CSS, HTML, XHTML, Ajax, ASP, ASP.NET, Dev

Posted: 9 Jun 2008
Updated: 9 Jun 2008
Views: 15,212
Bookmarked: 48 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
18 votes for this Article.
Popularity: 5.78 Rating: 4.61 out of 5
2 votes, 11.1%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
1 vote, 5.6%
4
15 votes, 83.3%
5
Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report This Article
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 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(); // The function SomeJS is available in Some.js only
});

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 function SomeJS is available in Some.js only
}); 

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()
{
    // 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, 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");
        
    // 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 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()
    {
        // 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 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);
                                })
                });
            }            
        });
        
        // 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 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) )
            {
                // 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 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);
              })
      });
    }            
  });
  
  // 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:

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

License

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

About the Author

Omar Al Zabir


I: Co-founder and CTO of www.pageflakes.com and Visual C# MVP.

My Blog: http://msmvps.com/blogs/omar/
My Specialization: Web 2.0 Rich AJAX Applications.
My Book: Building a Web 2.0 portal using ASP.NET 3.5. Also on Amazon
My Site: www.oazabir.com
My Email: OmarALZabir at gmail dot com
My Interest: Travel, Performance and Scalability Challenges.

My Projects:

Open Source Web 2.0 AJAX Portal
Smart UML - Freehand UML Designer
RSS Aggregator both Outlook and Standalone
Store Front in JSP but ASP.NET style

My Articles:

10 ASP.NET Performance and Scalability Secrets
ASP.NET AJAX under the hood secrets
UFrame: goodness of UpdatePanel and IFRAME combined
Fast ASP.NET web page loading
Fast, Scalable, Streaming AJAX Proxy
Using COM safely inside "using" block without requiring interop assembly
Implementing Word Like Automation Model
Distributed Command Pattern
StickOut - .NET 2.0, VSTS, Outlook Addin, MS Word/Excel integration
Deployment made simple with Powershell
Build Google IG like Ajax Start Page in 7 days
Occupation: Chief Technology Officer
Company: Pageflakes Inc
Location: Bangladesh Bangladesh

Other popular Ajax and Atlas articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 16 of 16 (Total in Forum: 16) (Refresh)FirstPrevNext
GeneralCachingmemberweblivz9:06 1 Sep '08  
GeneralQuestion about the callback...memberJohn Skrotzki17:16 11 Jul '08  
AnswerRe: Question about the callback...memberOmar Al Zabir19:35 11 Jul '08  
GeneralRe: Question about the callback...memberJohn Skrotzki12:49 12 Jul '08  
AnswerRe: Question about the callback...memberOmar Al Zabir5:36 14 Jul '08  
QuestionIt's for real?!?memberMember 455246910:24 30 Jun '08  
AnswerRe: It's for real?!?memberOmar Al Zabir20:26 30 Jun '08  
GeneralGreat stuf but what about the next time you visit the page?memberrobsworld20032:46 12 Jun '08  
GeneralRe: Great stuf but what about the next time you visit the page?memberrobsworld20032:50 12 Jun '08  
AnswerRe: Great stuf but what about the next time you visit the page?memberOmar Al Zabir3:07 12 Jun '08  
GeneralRe: Great stuf but what about the next time you visit the page?memberrobsworld200315:01 12 Jun '08  
Generalexcellent~~!memberhuobazi@aspxboy.com15:42 9 Jun '08  
GeneralBrilliantmemberSpeednet_12:09 9 Jun '08  
GeneralIngeniousmemberOskar Austegard6:56 9 Jun '08  
GeneralAwesome!membermerlin9815:11 9 Jun '08  
GeneralEnsure looks promisingmemberMadhur Ahuja4:43 9 Jun '08  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 9 Jun 2008
Editor:
Copyright 2008 by Omar Al Zabir
Everything else Copyright © CodeProject, 1999-2008
Web18 | Advertise on the Code Project