Normalizing Untrusted Deferred / Promise Values For The $digest Lifecycle In AngularJS
The other day, in my blog post about the $q.when() method in AngularJS, Jordan brought up the question at to what the AngularJS documentation meant when it referred to, "the promise comes from a source that can't be trusted." My guesstimation of this statement was that we couldn't trust that the promise was properly integrated with the $digest lifecycle, which is automatically triggered when a deferred value changes state (and has at least one bound handler). By wrapping an untrusted promise inside an AngularJS promise, we can ensure that state changes lead to dirty-checking of the data.
Run this demo in my JavaScript Demos project on GitHub.
Since I live in AngularJS, I couldn't really come up with a great example of when this would be necessary; so, I just put together an demo that uses a jQuery Deferred value, such as one that might be returned by a jQuery plugin. Then, I "wrap" the jQuery Deferred value in an AngularJS deferred value using the $q.when() method:
<!doctype html> | |
<html ng-app="Demo"> | |
<head> | |
<meta charset="utf-8" /> | |
<title> | |
Normalizing Untrusted Deferred / Promise Values In AngularJS | |
</title> | |
</head> | |
<body ng-controller="AppController"> | |
<h1> | |
Normalizing Untrusted Deferred / Promise Values In AngularJS | |
</h1> | |
<p> | |
Deferred value: {{ resolvedValue }} | |
</p> | |
<!-- Load scripts. --> | |
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script> | |
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script> | |
<script type="text/javascript"> | |
// Create an application module for our demo. | |
var app = angular.module( "Demo", [] ); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I control the root of the application. | |
app.controller( | |
"AppController", | |
function( $scope, $q, time ) { | |
$scope.resolvedValue = "null"; | |
// I return an "untrusted" deferred value. | |
// -- | |
// This would be any kind of deferred value that is not strictly created | |
// by AngularJS itself. Since AngularJS is tightly integrated with the | |
// $digest lifecycle, any externally-generated deferred value lack that | |
// level of integration. | |
var unsafeDeferred = (function getUnsafeDeferredFromJQuery() { | |
// For the "untrusted" demo, use the jQuery Deferred factory. | |
var deferred = jQuery.Deferred(); | |
setTimeout( | |
function resolveOperator() { | |
deferred.resolve( "jQuery Woot!" ); | |
}, | |
3000 | |
); | |
return( deferred ); | |
})(); | |
console.log( "Received unsafe promise at", time() ); | |
// Since we are using a Deferred value that was generated outside of | |
// AngularJS (via jQuery in this case), it is not to be trusted. As | |
// such, we have to wrap it in a $q-based deferred value so that it will | |
// normalize it for use within the AngularJS application. | |
$q.when( unsafeDeferred ).then( | |
function handleResolve( value ) { | |
console.log( "Unsafe promise resolved at", time() ); | |
$scope.resolvedValue = value; | |
} | |
); | |
} | |
); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I am just a utility service that returns the current time string. | |
app.factory( | |
"time", | |
function() { | |
return( time ); | |
function time() { | |
return( ( new Date() ).toTimeString().split( " " ).shift() ); | |
} | |
} | |
); | |
</script> | |
</body> | |
</html> |
As you can see, the resolution handler for the deferred value updates the view-model, which is rendered in the view. If I had bound my resolution handler directly to the "unsafeDeferred" value, the view would not have been re-rendered, as no $digest cycle would have been triggered. But, by normalizing the unsafe promise, I am hooking into the $digest lifecycle and the view is synchronized with the view-model as you would expect:

If I had not normalized the promise coming out of jQuery, the console.log() statements would have executed - the handlers still run - but the view would not have been updated. You could have explicitly triggered a scope.$apply() or a scope.$digest(); but, generally speaking, if you need to explicitly trigger a digest in your controllers, something else is probably going wrong.
Want to use code from this post? Check out the license.
Reader Comments
Another use for $q.when() I stumbled across recently is when you need an optional first link in a Promise chain. Something like:
function findAll(params, options) {
var deferred = $q.defer();
$q.when()
.then(function() {
if (params && params.postId) {
return Post
.find(params.postId)
.then(function(post) {
params.userId = post.userId;
});
}
})
.then(function() {
return Comment.findAll(params, options);
})
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
In this case if a particular post is specified, it will only return comments belonging to the post's creator.
@Ben
Great follow-up to our discussion. You are always very thorough :)
@Jordan,
Thank you good sir, and thanks for the thought-provoking conversation.
@Rob,
Very cool thought. Yeah, that's one of the really nice things about promises - they will resolve for everything *except* errors and explicitly rejected-values. This means that returning "nothing", is the same as *resolving* with nothing. I love promises.