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.
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);
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); }); }
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.Promise.all
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.Promise.all([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> 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.setHTML( 'Looks like the service might be down - would you like to <a href="?mock=true">try this example with mock data</a>?' ); } log('Fetching GitHub data for users: "yui", "yahoo" and "davglass"...') // requests is a regular promise var requests = Y.Promise.all([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>