Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jeff Coughlin
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jeff Coughlin ( @jeffcoughlin )

Watching Object Literal Expressions In AngularJS

By on

The other day, when I created a pixel-based version of ngStyle, I did something in AngularJS that I had never done before: I watched an expression that consisted of an Object literal. This works just like watching rgular $scope references; but, it has a few caveats that I thought I would share.

Run this demo in my JavaScript Demos project on GitHub.

I don't think there is any reason that I would ever have to watch an object literal expression inside of a Controller or a Service; but, in a Directive it makes sense. It allows the user to define an object in an Element attribute - think ngClass and ngStyle - which provides a good deal of flexibility and readability.

In my experiments, the major caveat with watching an object literal expression is that AngularJS creates a new object every time the $watch() expression is checked. This means that you can't use reference-based equality in your $watch() configuration. If you do, the newValue will always be different than the oldValue and the $digest phase will never end (without error). Instead, when watching an object literal expression, you have to use deep-object-equality. This compares the newValue and the oldValue based on the actual structure of the object and not just on its reference.

To see this in action, take a look at this simple demo where I define a $watch() on an object literal expression. Note that I am passing in "true" as the third argument of the $watch() configuration - this tells AngularJS to use that deep-object-equality.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Watching Object Literal Expressions In AngularJS
	</title>
</head>
<body ng-controller="AppController">

	<h1>
		Watching Object Literal Expressions In AngularJS
	</h1>


	<!-- 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.2.4.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, $parse ) {

				$scope.friend = {
					id: 4,
					name: "Heather"
				};


				// When we parse an AngularJS expression, we get back a function that
				// will evaluate the given expression in the context of a given $scope.
				var getter = $parse( "{ name: friend.name }" );

				// Get the result twice.
				var a = getter( $scope );
				var b = getter( $scope );

				// Check to see if evaluating the AngularJS expression above returns a
				// new object each time.
				// --
				// HINT: It does (return a new object each time).
				console.log( "Objects are equal:", ( a === b ) );


				// Since a new object is returned each time the Object Expression is
				// evaluated by AngularJS, we havd to use DEEP object equality.
				// Otherwise, the object reference will be different on EACH $digest
				// iteration, which will cause the digest to run forever (or rather,
				// to error out).
				$scope.$watch(
					"{ name: friend.name }",
					function( newValue, oldValue ) {

						console.log( "Watch:", newValue.name );

					},

					// Deep object equality.
					true
				);

			}
		);

	</script>

</body>
</html>

I said that you would probably never run this in a Controller; but, I'm using a controller here just to keep the code simple. And, when we run the above code, we get the following console output:

Objects are equal: false
Watch: Heather

As you can see, subsequent calls to the $parse-based "getter" return a new object reference each time. But, the deep-object-equality allows our $watch() handler to ben called only once since the "value" of the object never changes.

The nice thing about passing an object literal expression into a Directive is that it allows you to consolidate all of the values related to your directive. This is definitely something that I want to explore a bit further.

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

Reader Comments

1 Comments

Does this still apply if you use a function as the first argument to the $watch function?

$scope.$watch(
function(){ return friend.name; },
function( newValue, oldValue ) {
...

15,674 Comments

@M,

When you pass in a function as the watch "expression", the function will get called on *every* digest; however, your watch handler (the function that gets passed the new/old values) will only get invoked when the value you *return* from your watch expression function changes. So, to answer your question, No - this does not apply for your case. In the example you have, "friend.name" will be evaluated and return on each digest; but, unless the name actually changes, your callback handler won't be invoked.

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