Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Emily Meyer
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Emily Meyer

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

By
Published in Comments (4)

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.

Want to use code from this post? Check out the license.

Reader Comments

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

15,912 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel