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.