Tuesday, June 12, 2012

How to detect when a JavaScript or CSS file has been fully loaded dynamically

In a previous post I have described how to dynamically load JavaScript or CSS files into an HTML page. After we have loaded the files, we obviously want to start using them, i.e. run some loaded script function, or layout an HTML element by assigning a loaded CSS class to it.
But how do we know when a script or CSS file has been fully loaded by the browser and ready to use? The JavaScript case is easy to solve, that is if you control a script's source code. If you don't, you will have to fall back on some browser-dependent event handlers.
If you own the contents of the JavaScript file being loaded, a much simpler approach suffices. Let's assume you always want to call the same callback function when a script is loaded. If that's the case, just put a call to the callback function on the last line of the JavaScript file. After the rest of the file has been loaded, the callback function gets called.

Here is an example of a script file being loaded dynamically:

function initGui()
{
    // ...
    // Lots of code
    // ...
}

// ...
// More function and global variable definitions
// ...

// Notify the loading page that the script has been fully loaded.
// That page can now safely call the 'initGui' function above.
callback();

Now for the CSS case. As far as I know, there is no reliable 'load' indicator for CSS files that are included dynamically. This is confirmed by Stoyan's post. I agree with that post that 'to check for changes in the styling of a specific element' (the fifth of the 'Options for the magic part') is a bad idea. However, the idea inspired me to try a technique that also depends on an element's style becoming available after the CSS file that contains it has been loaded. And thus, we actually know that the file has been loaded!

The technique that I describe below is still in an experimental stage, and can only be used if you have control over the content of the CSS file (which is the case in most of my own projects). I have successfully tested it in the latest versions of Chrome, Firefox and Safari (for the desktop), and hope it will also work in the upcoming Internet Explorer 10. It depends on CSS3 transitions and the event that fires after a transition completes. At the end of the CSS file that is included dynamically in my HTML page, I define a 'helper' element that (for the moment) must contain several browser-specific transition properties:

/*
... Other CSS declarations ...
*/

/* CSS declarations of a helper element that must be added to the HTML page that uses the CSS file */
#css-loader
{
    width: 0px;
    height: 0px;
    visibility: hidden;
    transition: visibility 0.001ms;  /* Standard */
    -o-transition: visibility 0.001ms;  /* Opera */ 
    -ms-transition: visibility 0.001ms;  /* IE */ 
    -moz-transition: visibility 0.001ms;  /* Firefox */
    -webkit-transition: visibility 0.001ms;  /* Chrome and Safari */
}

Note the very small transition duration of 0.001ms. It is required since otherwise there will be no 'transition' at all and no 'transition end' event will be triggered. The css-loader helper element, that is visible by default when it is added to the brower DOM, is being hidden through its CSS declarations. This happens when the CSS file has been loaded, and triggers the 'transition end' event. Thus, when that event fires we know that the file has been loaded.

Here is the JavaScript that dynamically includes the CSS file. I have added comments to explain the code.

// Returns the browser-dependent event name that corresponds
// to the 'transition end' event.
function getTransitionEvent(helperElement) {
    var transitions = {
        'transition': 'transitionEnd',  // Standard
        'OTransition': 'oTransitionEnd',  // Opera
        'MSTransition': 'msTransitionEnd',  // IE
        'MozTransition': 'transitionend',  // Firefox
        'WebkitTransition': 'webkitTransitionEnd'  // Chrome and Safari
    }

    for (var t in transitions) {
        if (helperElement.style[t] !== undefined) {
            return transitions[t];
        }
    }
    // In case the above loop didn't return a result:
    return 'transitionEnd';
}

// Dynamically loads the CSS file that corresponds to the specified URL.
// When the file has been loaded, the specified callback function
// is called.
function loadCss(url, callback) {
    // Create a helper element and append it to the HTML page
    // temporarily.
    var cssLoader = document.createElement('div');
    cssLoader.setAttribute('id', 'css-loader');
    document.body.appendChild(cssLoader);

    var transitionEnd = getTransitionEvent(cssLoader);
    cssLoader.addEventListener(transitionEnd, function () {
        // Clean up: remove the event handler from the helper element.
        this.removeEventListener(transitionEnd, arguments.callee, false);
        // Remove the helper element from the HTML document,
        // we no longer need it.
        document.body.removeChild(cssLoader);
        // Notify the calling function that the CSS file has been loaded.
        if (callback) {
            callback();
        }
    }, false);

    // Create a CSS 'link' element to be added
    // to the page's HEAD section.
    var head = document.head || document.getElementsByTagName('head')[0];
    var link = document.createElement('link');
    link.rel = 'stylesheet';
    link.type = 'text/css';
    link.href = url;

    // Load the CSS file. WebKit browsers (Chrome and Safari) seem to
    // need the setTimeout in order for the 'transition end' event to
    // be fired. I haven't yet been able to find out why.
    setTimeout(function () {
        head.appendChild(link);
    }, 0);
}

// Dynamically include a CSS file after the HTML page has finished
// loading. Specify the URL of the file and a callback function
// that should be called when the CSS file has been loaded completely.
window.addEventListener('load', function () {
    loadCss('test.css', function () { alert('CSS file loaded!'); });
});

Like I have said before, the above code is still in an experimental stage and there may be some major flaw in it that I haven't noticed. If you find one, please let me know by leaving a comment below.

No comments:

Post a Comment