Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Cyril Hanquez and Hugo Sombreireiro and Reto Aeberli and Steven Peeters and Guust Nieuwenhuis and Aurélien Deleusière

Watching Object Literal Expressions In AngularJS

By Ben Nadel 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.




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 ) {
...

Reply to this Comment

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

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.