Using Scope.$watch() To Watch Functions In AngularJS

Most of the time, when I use $watch() in AngularJS, I am watching a $scope reference contained in a String value. But, you can also watch a Function. When you do this, your $watch function gets called multiple times per digest; and, if it returns a value, that value gets used, by AngularJS, to determine if your $watch callback should be invoked. This opens up some interesting opportunities in your AngularJS application.

Run this demo in my JavaScript Demos project on GitHub.

When you $watch() a function, the function gets called multiple times per digest. When it is invoked, AngularJS passes-in the current $scope reference as the first argument. Not only does it mean that we can reference the proper scope from within the function body, it also means that we can watch any function that expects a $scope reference.

One such type of function is the result of the $interpolate() service. When you use the $interpolate() service, you pass in a string that uses interpolation. And what you get back is a function that takes a $scope and returns the value of the input, interpolated in the context of said scope. This set of inputs and outputs makes the $interpolate() function well suited for a $watch() function.

To see this in action, I've set up a demo that watches two functions. One is a function expression and the other is the result of the $interpolate() service:

<!doctype html> <html ng-app="Demo"> <head> <meta charset="utf-8" /> <title> Using Scope.$watch() To Watch Functions In AngularJS </title> <link rel="stylesheet" type="text/css" href="./demo.css"></link> </head> <body ng-controller="AppController"> <h1> Using Scope.$watch() To Watch Functions In AngularJS </h1> <p> <strong>Best Friend:</strong> <a ng-click="setBestFriend( tricia )" ng-class="{ best: tricia.isBestFriend }" >{{ tricia.name }}</a> or <a ng-click="setBestFriend( joanna )" ng-class="{ best: joanna.isBestFriend }" >{{ joanna.name }}</a> </p> <!-- 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.19.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, $interpolate ) { $scope.tricia = { name: "Tricia", isBestFriend: 1 }; $scope.joanna = { name: "Joanna", isBestFriend: 0 }; $scope.bestFriend = $scope.tricia; // Normally, we would $watch() a scope reference. But, in the following // cases, we're going to watch Functions. When we do this, the watch // function will be called mulitple times per-digest. If you return a // value from the watch function, this value will be used to determine if // the watch callback should be triggered (on value change event). // Notice that $scope is passed-in as the first argument. $scope.$watch( function( $scope ) { console.log( "Function watched" ); // This becomes the value we're "watching". return( "Function: Best friend is " + $scope.bestFriend.name ); }, function( newValue ) { console.log( newValue ); } ); // Since $scope is passed into the watch function (see example above), it // means that we can watch any function that expects the scope as an // argument and returns a value in response. A great example of this is // the $interpolate() function - it returns a function that expects the // scope and returns the result of the interpolated value in that scope // context. As such, it's suited perfectly to watch interpolated values. $scope.$watch( $interpolate( "Interpolate: Best frined is {{ bestFriend.name }}" ), function( newValue ) { console.log( newValue ); } ); // This is here as a "control" - not really part of the primary demo; // but, can show the enhanced readability that is provided by // $interpolate(), which is doing the same thing. $scope.$watch( "( 'Literal: Best frined is ' + bestFriend.name )", function( newValue ) { console.log( newValue ); } ); // --- // PUBLIC METHODS. // --- // I set the best friend. Since there can only be one best friend at a // time, the previous one is removed from grace. $scope.setBestFriend = function( friend ) { // Reset the current bestie. $scope.bestFriend.isBestFriend = false; // Set the new one. $scope.bestFriend = friend; $scope.bestFriend.isBestFriend = true; }; } ); </script> </body> </html>

When we run this demo and change the best-friend selection a few times, we get the following console log output:

Function watched

Function: Best friend is Tricia

Interpolate: Best frined is Tricia

Literal: Best frined is Tricia

Function watched

Function watched

Function: Best friend is Joanna

Interpolate: Best frined is Joanna

Literal: Best frined is Joanna

Function watched

Function watched

Function: Best friend is Tricia

Interpolate: Best frined is Tricia

Literal: Best frined is Tricia

Function watched

There's a couple of things to notice in this output. First, "Function watched" was logged 6 times. This is because, as I stated above, when you watch a function, it gets called multiple times per digest. The second thing to notice is that the $interpolate() function properly evaluated the interpolated expression in the context of the current scope. This is because the $scope value is passed into all functions that are being watched. And lastly, the return value of the $watch() function was piped into the $watch() callback. That's why we only see "Function: Best friend is Tricia" and "Interpolate: Best frined is Tricia" when the value was actually changed (or initialized).

This is really cool stuff. In the majority of cases, a simple string-based scope-reference is sufficient when it comes to observing changes in the scope. But, having the ability to watch a function opens up a lot more flexibility. And, the fact that the $scope reference is passed into the watch function makes it even easier to use.

Tweet This Provocative thoughts by @BenNadel - Using Scope.$watch() To Watch Functions In AngularJS Woot woot — you rock the party that rocks the body!







