Watching Object Literal Expressions In AngularJS
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
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 ) {
...
@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.