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.