Short post just to extoll the value that using Q brings to my project.
In stackd.io, we adhere, as closely as possible, to the core REST principles in our API. One of the ramifications is that it sometimes makes building objects on the front end more time consuming. Here's what I mean.
Let's work with Fruits. Here's what a request to /api/fruits/
might return.
{
fruits: [
{
id: 'http://www.fruitmonkey.com/api/fruits/1/',
key: 1,
title: 'Banana',
description: 'Yellow fruit preferred by monkeys',
links: [
{
rel: "self",
href: "http://www.fruitmonkey.com/api/fruits/1/"
},
{
rel: "collection",
href: "http://www.fruitmonkey.com/api/fruits/"
},
{
rel: "history",
href: "http://www.fruitmonkey.com/api/fruits/1/history/"
},
{
rel: "sources",
href: "http://www.fruitmonkey.com/api/fruits/1/sources/"
}
]
}
]
}
Now, on the client, when it requests all Fruits from the API, each one is sparsely defined. This means that if I want the history and the sources of each fruit to be populated from the get go, then I need to make two additional requests for each fruit.
Obviously, I wouldn't do that in all cases, and only grab those specific details when they are needed to be displayed. However, let's assume that on the very first view that users see, the history has be visible. This means, that when I load all fruits, I must then loop through the resulting array and call the history URL and populate a new key on the fruit. Let's call it fruit.fullHistory
.
Now, I'm using promises even during the initial load process to handle dependency chaining, so my API.Fruits.populate()
method returns a promise itself.
API/Fruits.js
api.populate = function () {
var deferred = Q.defer();
$.ajax({
url: '/api/fruits/',
type: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
success: function (response) {
var fruits = response.results;
// Empty out the store of fruits
stores.Fruits.removeAll();
// Add each fruit in the response to the store
fruits.forEach(function (fruit) {
stores.Fruits.push(new models.Fruit().create(fruit));
});
deferred.resolve();
}
});
return deferred.promise;
};
So how to I get the history onto each Fruit before the promise is resolved? When I call the API endpoint to grab the history for each one inside that forEach()
method, it's an XHR request so the deferred.resolve()
will be executed before all those requests complete.
Here's where we can add more promises, and wait for them all to execute before the main deferred.resolve()
is executed. Here's the updated code for the success handler.
success: function (response) {
var fruits = response.results;
var historyPromises = []; // Will hold all history promises
stores.Fruits.removeAll();
fruits.forEach(function (fruit) {
/*
* Instead of firing off n sequential XHR requests,
* which will not defer the main promise from getting
* resolved until all fruits have been updated, I push
* the calls to getHistory() into an array which will
* be used by Q.all() below.
*/
historyPromises.push(api.getHistory(fruit)
.then(function (f) {
stores.Fruits.push(new models.Fruit().create(f));
}));
});
/*
* Here's where the calls to getHistory() actually get executed,
* and not until all of them resolve does this method's main
* promise get resolved. Confused yet?
*/
Q.all(historyPromises).then(deferred.resolve);
}
And here's what the getHistory()
method looks like.
api.getHistory = function (fruit) {
var deferred = Q.defer();
$.ajax({
url: _.findWhere(fruit.links, { rel: 'history' }),
type: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
success: function (response) {
fruit.fullHistory = response.results;
deferred.resolve(fruit);
}
});
return deferred.promise;
};
Since each call to getHistory()
returns a promise, I can use Q.all()
to wait until each promise is resolved before resolving the initial load of all fruits. Now, when my data binding library updates the UI, the fullHistory
key is populated with an array which I can reference in the corresponding HTML decorator.
<tbody data-bind="foreach: stores.Fruits">
<tr>
<td data-bind="text: title"></td>
<td data-bind="text: description"></td>
<td data-bind="template: { foreach: fullHistory }">
<span data-bind="text: eventTitle"></span>
</td>
</tr>
</tbody>