#

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>