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 the New York Javascript Meetup (Aug. 2010) with:

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.