Jump to Table of Contents

Example: Subclassing Y.Promise

This example expands on the Wrapping async transactions with promises example to illustrate how to create your own Promise subclass for performing operations on arrays.

Subclassing Y.Promise

You can subclass a YUI promise with Y.extend the same way you would any other class. Keep in mind that Promise constructors take a function as a parameter so you need to call the superclass constructor in order for it to work.

function ArrayPromise() {
    ArrayPromise.superclass.constructor.apply(this, arguments);
}
Y.extend(ArrayPromise, Y.Promise);

Method Chaining

Chaining promise methods is done by returning the result of calling the promise's then() method. then() always returns a promise of its same kind, so this will allow us to chain array operations as if they were real arrays.

For the purpose of this example we will only add the each, filter and map methods from the array-extras module.

// Although Y.Array.each does not return an array, for the purpose of this
// example we make it chainable by returning the same array
ArrayPromise.prototype.each = function (fn, thisObj) {
    return this.then(function (array) {
        Y.Array.each(array, fn, thisObj);
        return array;
    });
};

// Y.Array.map returns a new array, so we return the result of this.then()
ArrayPromise.prototype.map = function (fn, thisObj) {
    return this.then(function (array) {
        // By returning the result of Y.Array.map we are returning a new promise
        // representing the new array
        return Y.Array.map(array, fn, thisObj);
    });
};

// Y.Array.filter follows the same pattern as Y.Array.map
ArrayPromise.prototype.filter = function (fn, thisObj) {
    return this.then(function (array) {
        return Y.Array.filter(array, fn, thisObj);
    });
};

Finally we need a simple way to take a promise that we know contains an array and create an ArrayPromise with its value.

// Takes any promise and returns an ArrayPromise
function toArrayPromise(promise) {
    return new ArrayPromise(function (fulfill, reject) {
        promise.then(fulfill, reject);
    });
}

Putting our Class to Action

There are many cases in which you would want to work on asynchronous array values. Performing more than one async operation at a time and dealing with the result is one common use case. Y.batch waits for many operations and returns a promise representing an array with the result of all the operations, so you could wrap it in an ArrayPromise to modify all those results.

We will use the JSONP Cache from the previous example and make several simultaneous requests.

log('Fetching GitHub data for users: "yui", "yahoo" and "davglass"...')

// requests is a regular promise
var requests = Y.batch(GitHub.getUser('yui'), GitHub.getUser('yahoo'), GitHub.getUser('davglass'));
// users is now an ArrayPromise
var users = toArrayPromise(requests);

// Transform the data into a list of names
users.map(function (data) {
    log('Getting name for user "' + data.login + '"...')
    return data.name;
}).filter(function (name) {
    log('Checking if the name "' + name + '" starts with "Y"...')
    return name.charAt(0) === 'Y';
}).then(function (names) {
    log('Done!');
    return names;
}).each(function (name, i) {
    log(i + '. ' + name);
}).then(null, function (error) {
    // if there was an error in any step or request, it is automatically
    // passed around the promise chain so we can react to it at the end
    showError(error.message);
});

Full Example Code

<script>
YUI().use('promise', 'jsonp', 'node', 'array-extras', function (Y) {

function ArrayPromise() {
    ArrayPromise.superclass.constructor.apply(this, arguments);
}
Y.extend(ArrayPromise, Y.Promise);

// Although Y.Array.each does not return an array, for the purpose of this
// example we make it chainable by returning the same array
ArrayPromise.prototype.each = function (fn, thisObj) {
    return this.then(function (array) {
        Y.Array.each(array, fn, thisObj);
        return array;
    });
};

// Y.Array.map returns a new array, so we return the result of this.then()
ArrayPromise.prototype.map = function (fn, thisObj) {
    return this.then(function (array) {
        // By returning the result of Y.Array.map we are returning a new promise
        // representing the new array
        return Y.Array.map(array, fn, thisObj);
    });
};

// Y.Array.filter follows the same pattern as Y.Array.map
ArrayPromise.prototype.filter = function (fn, thisObj) {
    return this.then(function (array) {
        return Y.Array.filter(array, fn, thisObj);
    });
};

// Takes any promise and returns an ArrayPromise
function toArrayPromise(promise) {
    return new ArrayPromise(function (fulfill, reject) {
        promise.then(fulfill, reject);
    });
}


// A cache for GitHub user data
var GitHub = (function () {

    var cache = {},
        githubURL = 'https://api.github.com/users/{user}?callback={callback}';

    function getUserURL(name) {
        return Y.Lang.sub(githubURL, {
            user: name
        });
    }

    // Fetches a URL, stores a promise in the cache and returns it
    function fetch(url) {
        var promise = new Y.Promise(function (fulfill, reject) {
            Y.jsonp(url, function (res) {
                var meta = res.meta,
                    data = res.data;

                // Check for a successful response, otherwise reject the
                // promise with the message returned by the GitHub API.
                if (meta.status >= 200 && meta.status < 300) {
                    fulfill(data);
                } else {
                    reject(new Error(data.message));
                }
            });

            // Add a timeout in case the URL is completely wrong
            // or GitHub is too busy
            setTimeout(function () {
                // Once a promise has been fulfilled or rejected it will never
                // change its state again, so we can safely call reject() after
                // some time. If it was already fulfilled or rejected, nothing will
                // happen
                reject(new Error('Timeout'));
            }, 10000);
        });

        // store the promise in the cache object
        cache[url] = promise;

        return promise;
    }

    return {
        getUser: function (name) {
            var url = getUserURL(name);

            if (cache[url]) {
                // If we have already stored the promise in the cache we just return it
                return cache[url];
            } else {
                // fetch() will make a JSONP request, cache the promise and return it
                return fetch(url);
            }
        }
    };
}());


var demoNode = Y.one('#demo');

function log(text) {
    demoNode.append(Y.Node.create('<div></div>').set('text', text));
}
function showError(message) {
    demoNode.append(
        Y.Node.create('<div class="error"></div>').set('text', message)
    );
}

log('Fetching GitHub data for users: "yui", "yahoo" and "davglass"...')

// requests is a regular promise
var requests = Y.batch(GitHub.getUser('yui'), GitHub.getUser('yahoo'), GitHub.getUser('davglass'));
// users is now an ArrayPromise
var users = toArrayPromise(requests);

// Transform the data into a list of names
users.map(function (data) {
    log('Getting name for user "' + data.login + '"...')
    return data.name;
}).filter(function (name) {
    log('Checking if the name "' + name + '" starts with "Y"...')
    return name.charAt(0) === 'Y';
}).then(function (names) {
    log('Done!');
    return names;
}).each(function (name, i) {
    log(i + '. ' + name);
}).then(null, function (error) {
    // if there was an error in any step or request, it is automatically
    // passed around the promise chain so we can react to it at the end
    showError(error.message);
});

});
</script>