Binding in AngularJS works magically. When an element in the controller changes the view is updated automatically. When an input in the view is updated, the controller value is automatically updated.

This is because every time the user interacts with the app, AngularJS automatically runs the $digest() method which automatically updates all bindings.

I am working on an app for a client that needed to execute JavaScript code when a binding changes. I had one view, and controller, which displayed some data filtering options to the user. A second view, and controller, displayed data loaded from the remote services, based on the filtering options. When the filtering elements were changed, a remote service call was made in the filtering view, and the results needed to modify the data view.

However, I discovered that when the results came back from the service call, the view was not updating with the new data. Why is that?

There are a few things at play here and I'm going to start with some simple examples of binding, then expand to discuss $watch(), and finally I'll talk about how to use $apply() to solve the problem.

Setting up a Simple Application with Binding

To demonstrate watches I'm going to start with a simple application. It will take two inputs from a user; a first name and a last-name. Then it will display the values to the user, updating as the user types.

First, create a simple HTML page. Then import the Angular library:

view plain print about
1<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js" ></script>

Then create another script block, with a module and controller:

view plain print about
1<script>
2 var watchTest = angular.module('WatchTest',[]);
3
4 watchTest.controller('watchTestCtrl',['$scope', function($scope){
5 }]);
6 </script>

Inside the controller, add some scope variables. One for the first name and one for the last name:

view plain print about
1$scope.firstName = "Jeffry";
2 $scope.lastName = "Houser";

I'm also going to add a reset function:

view plain print about
1$scope.onReset = function(){
2 $scope.firstName = "";
3 $scope.lastName = "";
4 }

The HTML behind this application will put the ng-app tag on the body, and point the watchTestCtrl on the div tag:

view plain print about
1<body ng-app="WatchTest">
2 <div ng-controller="watchTestCtrl">
3 First Name: <input type="text" ng-model="firstName" /><br/>
4 Last Name: <input type="text" ng-model="lastName" /><br/>
5 Hello: {{firstName}} {{lastName}}<br/>
6 <Br/>
7 <button ng-click="onReset()" >Reset</button>
8 </div>
9</body>

See this app in action or play with the Plunker.

Setting up a Simple Watch

AngularJS binding, with ng-model and the double curly bracket syntax, does not allow you to run JavaScript code when a value changes. Thankfully, it does provide a $scope function, $watch(), for this purpose.

Let's set up a simple watch to show how we can run additional code when values change.

Inside the controller, add the watch:

view plain print about
1$scope.$watch( 'firstName',
2 function(newValue, oldValue){
3 console.log('firstName Changed');
4 console.log(newValue);
5 console.log(oldValue);
6 }
7 );
8
9 $scope.$watch( 'lastName',
10 function(newValue, oldValue){
11 console.log('lastName Changed');
12 console.log(newValue);
13 console.log(oldValue);
14 }
15 );

The $watch() is a function on the $scope service. It accepts two arguments. The first is a string, representing the value that you want to watch for changes. This could also be a function that returns the value you want to watch. The second is a function which is executed whenever the value changes. The arguments for the change function are the newValue and the oldValue.

For the sake of the example, I just logged the new and old values to the console; however you could perform other functionality, such as instigating a remote service call or other processing.

You can try to run this app or play with the Plunker. You'll need to keep the web developer console of your browser open to see the log statements.

The problem with Binding and Asynchronous calls

For binding, or the $watch() method, to execute the variable change must happen within the context of an AngularJS $digest(). Angular automatically runs $digest() whenever user interaction takes places which affects the $scope. However, the $digest() will not occur without user interaction. When retrieving, or changing, data asynchronously, the $digest() will not be triggered, the bindings will not update, and the $watch() method never fires.

To demonstrate the problem, I am going to use the JavaScript setTimeout() function instead building out a full remote service call. First, I'm going to add a JavaScript method to change the firstName with the setTimeout() function:

view plain print about
1$scope.triggerChange = function () {
2 setTimeout(function(){
3 console.log('First name being reset');
4 $scope.firstName = '';
5 }, 1000);
6 };

I'm also going to add a button to trigger that change:

view plain print about
1<button ng-click="triggerChange()" >Trigger Change</button>

So, when you click the "Trigger Change" button in the UI; the triggerChange() method will be executed, queuing up the timeout function to run in 1 second. When it runs, we see a console.log() and the value is changed. Try this and you'll see that the values in the UI are not updated even though you can see the log in the console, proving that the element did execute.

As a helper, you can add a method to log the user information:

view plain print about
1$scope.logName = function () {
2 console.log($scope.firstName);
3 console.log($scope.lastName);
4 }

And add a button to trigger this:

view plain print about
1<button ng-click="logName()" >Log Name</button>

Click the logName button, and you'll see that the values in the UI automatically get updated. This is because the user interaction forced the $digest() method to execute, which updated the bindings, and also caused the $watch() function to execute.

You can Test the app or play with the Plunker

Forcing the update with $apply() method on $scope

The simplest solution to this is to use Angular's $apply() method on the $scope. The $apply() method accepts a single argument, which is a function. The function executes, then the $digest() functionality will be executed for the variables that changed. We can change the trigger function, like this:

view plain print about
1$scope.triggerChangeWithApply = function () {
2 setTimeout(function(){
3 console.log('First name being reset');
4 $scope.$apply(function(){
5 $scope.firstName = ''
6 }
7 )
8 }, 1000);
9 };

Instead of changing the $scope.firstName directly in the timeout function, it is done inside he $apply() function. The $apply() function makes sure that Angular's bindings update. Retest the code, and you'll see that after the timeout function runs, the UI is automatically updated. Play with the Plunker

What do you do when all your code is encapsulated into an Angular Service?

The original use case I mentioned was when a remote service responded with asynchronous events. If you're building a larger application, there is a good chance that your remote service code will be encapsulated out of a controller and into an AngularJS Service. AngularJS Services do not have access to a $scope service because they do not have a view. As such, you cannot directly use $apply() to update bindings. What do you do when you want to update the view in response to an event handler inside of an Angular service?

I have two solutions, and I'm not sure which is better. The first is to add a callback to the function which resides inside the controller. Pass the callback as an argument into the service, and execute the callback when you get results. The callback method can call $apply() on the $scope. Something like this would work:

view plain print about
1var callback = function(){
2 $scope.$apply()
3}

In this case, a function is not used as an argument to $apply(). Angular will automatically figure out everything that has changed. The second solution is to grab a UI Element and find the scope on that, something like this.

view plain print about
1var element = angular.element($('#MyElementWithController'));
2element.scope().$apply();

This method works, and prevents the need for a callback just to update, however I'm unclear if it is the best approach. Ideally you wouldn't want the services trying to access the HTML directly for any reason.

Final Thoughts

I've had a lot of fun with this, but don't have any other thoughts to share.

Did I mention that you should all check out the free AngularJS training books I wrote at training course on AngularJS? You can get some books I wrote for free.

Sign up for DotComIt's Monthly Technical Newsletter