Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

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.

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