AngularJS Services for Working with Promises

Promises are a way of registering interest in something that will happen in the future, such as the response sent from a server for an Ajax request. Promises are not unique to AngularJS, and they can be found in many different libraries, including jQuery, but there are variations between implementations to accommodate differences in design philosophy or the preferences of the library developers.

There are two objects required for a promise: a promise object, which is used to receive notifications about the future outcome, and a deferred object, which is used to send the notifications. For most purposes, the easiest way to think of promises is to regard them as a specialized kind of event; the deferred object is used to send events via the promise objects about the outcome of some task or activity.

I am not being needlessly vague when I talk about “some task or activity” because promises can be used to represent anything that will happen in the future. And the best way to demonstrate this flexibility is with an example— but rather than show you another Ajax request, I am going to keep things simple and use button clicks. Listing 20-11 shows the contents of the promises.html file that I added to the angularjs folder. This is the initial implementation of an application to which I will add promises but which for the moment is just a regular AngularJS application.

Listing 20-11. The Contents of the promises.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Promises</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<button class=”btn btn-primary”>Heads</button>

<button class=”btn btn-primary”>Tails</button>

<button class=”btn btn-primary”>Abort</button>

Outcome: <span></span>

</div>

</body>

</html>

This is a trivially simple application that contains buttons marked Heads, Tails, and Abort and inline data binding for a property called outcome. My goal will be to use deferred and promise objects to wire up the buttons such that clicking one of them will update the outcome binding. Along the way, I’ll explain why promises are not like regular events. Figure 20-3 shows how the browser displays the promises.html file.

AngularJS provides the $q service for obtaining and managing promises, which it does through the methods that I have described in Table 20-8. In the sections that follow, I’ll show you how the $q service works as I build out the example application.

1. Getting and Using the Deferred Object

I am showing you both sides of a promise in this example, and that means I need to create a deferred object, which I will use to report on the eventual outcome when the user clicks one of the buttons. I obtain a deferred object through the $q.defer method, and a deferred object defines the methods and properties shown in Table 20-9.

The basic pattern of use is to get a deferred object and then call the resolve or reject method to signal the outcome of the activity. You can, optionally, provide interim updates through the notify method. Listing 20-12 shows how I have added a directive to the example that uses a deferred object.

Listing 20-12. Working with deferred Objects in the promises.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Promises</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.directive(“promiseWorker”, function($q) {

var deferred = $q.defer();

return {

link: function(scope, element, attrs) {

element.find(“button”).on(“click”, function (event) {

var buttonText = event.target.innerText;

if (buttonText == “Abort”) {

deferred.reject(“Aborted”);

} else {

deferred.resolve(buttonText);

}

});

},

controller: function ($scope, $element, $attrs) {

this.promise = deferred.promise;

}

}

})

.controller(“defaultCtrl”, function ($scope) {

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well” promise-worker>

<button class=”btn btn-primary”>Heads</button>

<button class=”btn btn-primary”>Tails</button>

<button class=”btn btn-primary”>Abort</button>

Outcome: <span></span>

</div>

</body>

</html>

The new directive is called promiseWorker, and it relies on the $q service. Within the factory function I call the $q.defer method to obtain a new deferred object so that I can access it within both the link function and the controller.

The link function uses jqLite to locate button elements and register a handler function for the click event. On receipt of the event, I check the text of the clicked element and call either the deferred object’s resolve method (for the Heads and Tails buttons) or reject method (for the Abort button). The controller defines a promise property that maps to the deferred object’s promise property. By exposing this property through the controller, I can allow other directives to obtain the promise object associated with the deferred object and receive the signals about the outcome.

Tip You should expose the promise object only to other parts of the application and keep the deferred object out of reach of other components, which would otherwise be able to resolve or reject the promise unexpectedly. This is partially why I assign the deferred object in Listing 20-12 within the factory function and provide the promise property only through the controller.

2. Consuming the Promise

The example application works at the point, in that the deferred object is used to signal the result of the user button click, but there is no one to receive those signals. The next step is to add another directive that will monitor the outcome through the promise created in the previous example and update the contents of the span element in the example. In Listing 20-13, you can see how I have created the required directive, which I have named promiseObserver.

Listing 20-13. Consuming a Promise in the promises.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Promises</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.directive(“promiseWorker”, function($q) {

var deferred = $q.defer(); return {

link: function(scope, element, attrs) {

element.find(“button”).on(“click”, function (event) {

var buttonText = event.target.innerText;

if (buttonText == “Abort”) {

deferred.reject(“Aborted”);

} else {

deferred.resolve(buttonText);

}

});

},

controller: function ($scope, $element, $attrs) {

this.promise = deferred.promise;

}

}

})

.directive(“promiseObserver”, function() {

return {

require: “^promiseWorker”, link: function (scope, element, attrs, ctrl) {

ctrl.promise.then(function (result) {

element.text(result);

}, function (reason) {

element.text(“Fail (” + reason + “)”)j })J

}

}

})

.controller(“defaultCtrl”, function ($scope) {

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well” promise-worker>

<button class=”btn btn-primary”>Heads</button>

<button class=”btn btn-primary”>Tails</button>

<button class=”btn btn-primary”>Abort</button>

Outcome: <span promise-observerx/span>

</div>

</body>

</html>

The new directive uses the require definition property to obtain the controller from the other directive and get the promise object. The promise object defines methods shown in Table 20-10.

Tip Notice that promise objects do not define the success and error methods that I used in the Ajax examples earlier in the chapter. These are convenience methods added to make using the $http service easier.

In the listing, I use the then method to register functions that will be called in response to the associated deferred object’s resolve and reject methods being called. Both of these functions update the contents of the element to which the directive has been applied. You can see the overall effect by loading the promises.html file into the browser and clicking one of the buttons, as shown in Figure 20-4.

3. Understanding Why Promises Are Not Regular Events

At this point, you might be wondering why I have gone to all the trouble of creating deferred and promise objects just to achieve something that could as easily be done with a regular JavaScript event handler.

It is true that promises perform the same basic function: They allow a component to indicate that it would like to be notified when something specific happens in the future, be that a button click or an Ajax result arriving from the server. Promises and regular events both provide the features required to register functions that will be invoked when the future thing happens (but not before). And, yes, I could have easily handled my button example using regular events—or even the ng-click directive, which relies on regular events but hides away the details.

It is only when you start to dig into the details that the differences between promises and events and the roles they play in an AngularJS application become apparent. In the sections that follow, I’ll describe the ways in which promises differ from events.

3.1. Use Once, Discard

Promises represent a single instance of an activity, and once they are resolved or rejected, promises cannot be used again. You can see this if you load the promises.html file into the browser and click the Heads button and then the Tails button. When you click the first button, the display is updated so that the outcome is shown as Heads. The second button click has no effect, and that’s because the promise in the example has already been resolved and can’t be used again; once set, the outcome is immutable.

This is important because it means that the signal sent to the observer represents “the first time that the user choses Heads or Tails or Aborts” If I used regular JavaScript click events, then each single would be simply “the user has clicked a button,” without any context about whether this is the first or tenth time that the user has clicked or what those clicks represented in terms of a user’s decision.

This is an important difference, and it makes promises suitable for signaling the outcome of specific activities, while events signal outcomes that can recur and even differ. Or, put another way, promises are more precise because they signal the outcome or result of a single activity, be that a user’s decision or the response for a particular Ajax requests.

3.2. Signals for Outcomes and Results

Events allow you to send a single when something happens—when a button is clicked, for example. Promises can be used in the same way, but they can also be used to signal when there is no outcome, either because the activity wasn’t performed or because the activity failed through the reject method in the deferred object, which triggers the error callback function registered with the promise object. You can see this in the example, where clicking the Abort button calls the reject button, which in turn updates the display to show that the user didn’t make a decision.

Being able to signal that the activity didn’t happen or that something went wrong ensures that you have a definite view of the outcome, which is important for activities such as making Ajax requests where you want to notify the user if there is a problem.

4. Chaining Outcomes Together

Having a definite view of the outcome, even when the activity wasn’t performed, allows for one of the best features of promises—the ability to chain promises together to create more complex arrangements of outcomes. This is possible because the methods defined by the promise object, such as then, return another promise, which is resolved when the callback function has completed execution. In Listing 20-14, you can see a simple example using the then method to chain promises together.

Listing 20-14. Chaining Promises in the promises.html File

<script>

angular.module(“exampleApp”, [])

.directive(“promiseWorker”, function($q) {

var deferred = $q.defer();

return {

link: function(scope, element, attrs) {

element.find(“button”).on(“click”, function (event) {

var buttonText = event.target.innerText;

if (buttonText == “Abort”) {

deferred.reject(“Aborted”);

} else {

deferred.resolve(buttonText);

}

});

},

controller: function ($scope, $element, $attrs) { this.promise = deferred.promise;

}

}

})

.directive(“promiseObserver”, function() {

return {

require: “^promiseWorker”, link:

function (scope, element, attrs, ctrl) {

ctrl.promise

.then(function (result) {

return “Success (” + result + “)”; }).then(function(result) { element.text(result);

});

}

}

})

.controller(“defaultCtrl”, function ($scope) { });

</script>

Within the link function of the promiseObserver directive, I obtain the promise and call the then method to register a callback function that will be invoked when the promise is resolved. The result from the then method is another promise object, which will be resolved when the callback function has been executed. I use the then method again to register a callback with the second promise.

Tip For simplicity, I have not included a handler for dealing with the promise being rejected, which means that this example will respond only to the Heads and Tails buttons being clicked.

Notice that the first callback function returns a result, as follows:

ctrl.promise.then(function (result) {

return “Success (” + result + “)”;

}).then(function(result) {

element.text(result);

});

When you chain promises together, you can manipulate the result that is passed along to the next promise in the chain. In this case, I do some simple formatting of the result string, which is then passed as the result to the next callback in the chain. Here is the sequence that occurs when the user clicks the Heads button:

  1. The promiseWorker link function calls the resolve method on the deferred object, passing in Heads as the outcome.
  2. The promise is resolved and invokes its success function, passing the Heads value.
  3. The callback function formats the Heads value and returns the formatted string.
  4. The second promise is resolved and invokes its success function, passing in the formatted string to the callback function as the outcome.
  5. The callback function displays the formatted string in the HTML element.

This is important when you want to set up a domino-effect of actions, where each action in the chain depends on the result of the previous outcome. My string formatting example is not compelling in this regard, but you can imagine making an Ajax request to obtain a URL of a service and passing this as the outcome to the next promise in the chain, whose callback will use the URL to request some data.

5. Grouping Promises

Chains of promises are useful when you want to perform a sequence of actions, but there are occasions when you want to defer an activity until the several other outcomes are available. You can do this through the $q.all method, which accepts an array of promises and returns a promise that isn’t resolved until all of the input promises are resolved. In Listing 20-15, I have expanded the example to use the all method.

Listing 20-15. Grouping Promises in the promises.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Promises</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.directive(“promiseWorker”, function ($q) {

var deferred = [$q.defer(), $q.defer()];

var promises = [deferred[0].promise, deferred[1].promise];

return {

link: function (scope, element, attrs) {

element.find(“button”).on(“click”, function (event) {

var buttonText = event.target.innerText;

var buttonGroup = event.target.getAttribute(“data-group”);

if (buttonText == “Abort”) {

deferred[buttonGroup].reject(“Aborted”);

} else {

deferred[buttonGroup].resolve(buttonText);

}

});

},

controller: function ($scope, $element, $attrs) {

this.promise = $q.all(promises).then(function (results) {

return results.join();

});

}

}

})

.directive(“promiseObserver”, function () {

return {

require: “^promiseWorker”, link: function (scope, element, attrs, ctrl) {

ctrl.promise.then(function (result) {

element.text(result);

}, function (reason) {

element.text(reason);

});

}

}

})

.controller(“defaultCtrl”, function ($scope) {

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well” promise-worker>

<div class=”btn-group”>

<button class=”btn btn-primary” data-group=”0″>Heads</button>

<button class=”btn btn-primary” data-group=”0″>Tails</button>

<button class=”btn btn-primary” data-group=”0″>Abort</button>

</div> <div class=”btn-group”>

<button class=”btn btn-primary” data-group=”1″>Yes</button>

<button class=”btn btn-primary” data-group=”1″>No</button>

<button class=”btn btn-primary” data-group=”1″>Abort</button>

</div>

Outcome: <span promise-observer></span>

</div>

</body>

</html>

In this example, there are two groups of buttons, allowing the user to choose Heads/Tails and Yes/No. In the promiseWorker directive, I create an array of deferred objects and an array of the corresponding promise objects. The promise that I expose via the controller is created using the $q.all method, like this:

this.promise = $q.all(promises).then(function (results) {

return results.join();

});

The call to the all method returns a promise that won’t be resolved until all of the input promises are resolved (which is the set of promise objects in the promises array) but will be rejected if any of the input promises are rejected. This is the promise object that the promiseObserver directive obtains and observes by registering success and error callback functions. To see the effect, load the promises.html file into the browser and click the Heads or Tails button followed by the Yes or No button. After you make the second selection, the overall result will be displayed, as shown in Figure 20-5.

The promise that I created with the $q.all method passes an array to its success function containing the results from each of the input elements. The results are arranged in the same order as the input promises, meaning that Heads/Tails will always appear first in the array of results. For this example, I use the standard JavaScript join method to concatenate the results and pass them to the next stage in the chain. If you look closely at this example, you will see that there are five promises:

  1. The promise that is resolved when the user selects Heads or Tails
  2. The promise that is resolved when the user selects Yes or No
  3. The promise that is resolved when promises (1) and (2) are both resolved
  4. The promise whose callback uses the join method to concatenate the results
  5. The promise whose callback displays the concatenated results in the HTML element

I don’t want to labor the point, but complex chains of promises can cause a lot of confusion, so here is the sequence of actions in the example, referencing the previous list of promises (I am assuming that the user choses Heads/Tails first, but the sequence is much the same if Yes/No is selected first):

  1. The user clicks Heads or Tails, and promise (1) is resolved.
  2. The user clicks Yes or No, and promise (2) is resolved.
  3. Promise (3) is resolved without any further user interaction and passes an array containing the results from promises (1) and (2) to its success callback.
  4. The success function uses the join method to create a single result.
  5. Promise 4 is resolved.
  6. Promise 5 is resolved.
  7. The success callback for promise (5) updates the HTML element.

You can see how a simple example can lead to complex combinations and chains of promises. This may seem overwhelming at first, but as you get used to working with promises, you will come to appreciate the precision and flexibility that they offer, which is especially valuable in complex applications.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *