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.