Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Adam Lewis
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Adam Lewis

Normalizing Untrusted Deferred / Promise Values For The $digest Lifecycle In AngularJS

By Ben Nadel on

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:


 
 
 

 
 Normalize non-trusted deferred / promise values inside AngularJS using $q.when(). 
 
 
 

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.




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.

Reply to this Comment

@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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.