CodeView

$.Promises: A wrapper for collections of jQuery Deferreds

promises.js

/**
 * # The $.Promises object
 * 
 * The $.Promises object is a convenience wrapper around arrays of jQuery
 * Deferreds or promises. It helps to collect Deferreds and add new ones later
 * on, to delay their resolution and pass them to $.when even before all
 * Deferreds of the collection are set up.
 *
 * In short, $.Promises adds another layer of asynchronicity to Deferreds and
 * provides an easy-to-read API at the same time.
 *
 * See
 *
 *     http://www.zeilenwechsel.de/it/articles/6/.Promises-Handling-collections-of-jQuery-Deferreds.html
 * 
 * for more information.
 *
 * ## Dependencies
 *
 * Requires jQuery 1.6.0 or newer.
 * 
 * ## Other
 *
 * @author  Michael Heim, http://www.zeilenwechsel.de/
 * @license MIT, http://www.opensource.org/licenses/mit-license.php
 * @version 0.1.0, 6 July 2011
 */
 
jQuery.extend( {
 
    Promises: ( function ( $ ) {
 
        var Promises = function () {
 
            var masterDfd = $.Deferred(),
                collected = [],
                counter = 0,
                block,
                blockIndex,
                ignoreBelatedCalls = false;
 
 
            // Make 'new' optional
            if ( ! ( this instanceof Promises ) ) {
 
                var obj = new Promises();
                return Promises.apply( obj, arguments );
                // ... re-runs the constructor function, same as
                // obj.constructor.apply( obj, arguments );
 
            }
 
 
            /**
             * Takes an array of objects and removes any duplicates. The first
             * occurrence of the object is preserved. The order of elements
             * remains unchanged. 
             * 
             * @param   {Array} arr
             * @returns {Array}
             */
            var toUniqueObjects = function ( arr ) {
 
                var unique = [],
                    duplicate,
                    i, j, len, uniqueLen;
 
                for ( i = 0, len = arr.length; i < len; i++ ) {
 
                    duplicate = false;
                    for ( j = 0, uniqueLen = unique.length; j < uniqueLen; j++ ) duplicate = ( arr[i] === unique[j] ) || duplicate;
                    if ( ! duplicate ) unique.push( arr[i] );
 
                }
 
                return unique;        
 
            };
 
            this.add = function () {
 
                if ( collected[0] && ! this.isUnresolved() && ! ignoreBelatedCalls ) {
                    throw {
                        name: 'PromisesError',
                        message: "Can't add promise when Promises is no longer unresolved"
                    };
                }
 
                for ( var i = 0; i < arguments.length; i++ ) collected.push( arguments[i] );
                collected = toUniqueObjects( collected );
 
                if ( collected.length ) {
 
                    counter++;
                    $.when.apply( this, collected )
                          .done( resolveIfCurrent( counter ) )
                          .fail( rejectIfCurrent( counter ) );
 
                }
 
                return this;
 
            };
 
            this.postpone = function () {
 
                if ( ! block ) {
 
                    if ( collected[0] && ! this.isUnresolved() && ! ignoreBelatedCalls ) {
                        throw {
                            name: 'PromisesError',
                            message: "Can't postpone resolution when Promises is no longer unresolved"
                        };
                    }
 
                    block = $.Deferred();
                    blockIndex = collected.length;
                    this.add( block );
 
                }
 
                return this;
 
            };
 
            this.stopPostponing = function () {
 
                if ( block ) {
 
                    collected.splice( blockIndex, 1 );
                    this.add();     // we don't add anything, but the masterDeferred will be updated
                    block = null;
 
                }
 
                return this;
 
            }
 
            this.ignoreBelated = function ( yesno ) {
 
                ignoreBelatedCalls = ! yesno;
                return this;
 
            };
 
            this.isUnresolved = function () {
 
                return ! ( this.isResolved() || this.isRejected() );
 
            }
 
            var resolveIfCurrent = function ( counterAtInvokation ) {
 
                return ( function() {
                    if ( counter == counterAtInvokation ) masterDfd.resolve.apply( this, arguments );
                } );
 
            };
 
            var rejectIfCurrent = function ( counterAtInvokation ) {
 
                return ( function() {
                    if ( counter == counterAtInvokation ) masterDfd.reject.apply( this, arguments );
                } );
 
            };
 
            this.add.apply( this, arguments );
 
            return masterDfd.promise( this );
 
        };
 
        return Promises;
 
    } )( jQuery )
 
} );