< Previous chapterNext chapter >
Last updated .
Having come so far in my mastery of Angular development, I still find it a bit unnerving that some things seem to happen as by magic. Something is going on behing my back and I want to know about it. This chapter is about data binding, the scope object, and its $watch and $apply methods.
Over the course of the previous chapters, there have been numerous examples of data binding with Angular. With data binding, the framework sees to it that a piece of data and the display of that data in the UI are kept in sync.
We have already seen that objects such as controllers and directives are associated with a scope. A scope object is really just a javascript object with properties, some of which may be functions. When using the controllerAs syntax, the controller ends up as a property on the scope. The name of the property is "vm". For example, if you have a "Name" property in scope it can be accessed as "$scope.vm.Name". Conversely, when not using controllerAs syntax, the Name property is accessed with "$scope.Name". Consider the following very simple example, where the scope of a controller has a single property which is then databound to two different UI elements:
DataBinding.html
<!doctype html>
<html>
<head>
<title>DataBinding</title>
<script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script>
</head>
<body ng-app="app" ng-Controller="controller as vm">
<input ng-model="vm.Name"/>
<span>{{ vm.Name }}</span>
<script src="DataBinding.js"></script>
</body>
</html>
DataBinding.js
(
function ()
{
var app = angular.module('app', []);
function Controller()
{
var vm = this;
}
app.controller("controller", [Controller]);
}
)();
As the user changes the contents of the input field, Angular updates the model (scope), which in turn updates back into the view with an update of the contents of the span element. This is data binding in action. There are plenty of details involved here, but my first question is: how does Angular even know that the user has typed in the input field? Well, it turns out that Angular sets event listeners on elements as it sees fit. In the present example, the Firefox Page Inspector lists five event handlers on the input field, all set to the same handler function in the Angular library:
I'm not sure why Angular wants to listen on these five events, I'd have thought the change event would be sufficient, but it doesn't matter. The point is that Angular is notified when the UI is updated.
In addition to the methods and properties you add to the scope object, it is born with quite a few members. One of these is the $watch method. That method can be called to attach a watcher to the scope. A watcher is something that is notified when a change occurs in the scope. It is composed of two function properties:
Another scope member is the $digest method. It's job is to run through all watchers on the scope, calling their getValue function to get the current value of the watched data. If that value has changed since last time, the listener function is called. For this to work, the watcher function has to keep a local copy of the watched data; after all, two values are needed to make a comparison - the new and the old value.
Supposedly, that $scope.$digest method is then called when a UI event happens, for example when the user checks a databound checkbox. But exactly when is it called? We can get notified of that by adding a watcher. Remember, $digest calls all watchers. To add a watcher, we just need to call the $watch method. As mentioned, this method takes two arguments - a getValue function and a listener function. We can omit the listener; we don't need it in this case.
DataBinding.html
<!doctype html>
<html>
<head>
<title>DataBinding</title>
<script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script>
</head>
<body ng-app="app" ng-Controller="controller as vm">
<input ng-model="vm.Name" />
<span>{{ vm.Name }} </span>
<input type="checkbox"/>
<button ng-click="vm.click()">Click me!</button>
<script src="DataBinding.js"></script>
</body>
</html>
DataBinding.js
(
function ()
{
var app = angular.module('app', []);
function Controller($scope)
{
var vm = this;
vm.click = function () { }
var counter = 0;
$scope.$watch(function ()
{
console.log("digest cycle " + counter++);
});
}
app.controller("controller", ["$scope", Controller]);
}
)();
It turns out that when the button is clicked, one digest cycle is run. When the checkbox is checked, no cycle is run - not surprising, since the checkbox isn't databound. Two digest cycles are run whenever I type in the input field. That is because Angular will continue to run digest cycles until no change is detected. The conclucion to draw from this, I guess, is that Angular surveilles everything under the root ng-app directive as needed. You might imagine, if that $digest cycle involves code that performs bad, it will have a negative influence on the whole user experience, since it is being called all the time.
You can use the $watch function to get notified of changes to the model. Having to do that is generally viewed as an indication of bad design or that you could accomplish the same by using other Angular constructs. Nevertheless, it is possible. For example, if in code you have to react to a change of a property, you can use the $watch function to register a listener function to be called when the property changes. Imagine that the user can enter numbers in two input fields, have their product computed and displayed in a result field. Of course, one could show that product with an Angular expression ( {{ vm.Product }} ), and everything would be automatic. But if that result field is outside of the Angular root scope, it must be done with good old DOM manipulation. An example:
watch.html
<!doctype html>
<html>
<head>
<title>Watch</title>
<script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script>
</head>
<body>
Factor1 * Factor2 = <span id="computed-value"></span>
<div ng-app="app" ng-Controller="controller as vm">
Factor1: <input type="number" ng-model="vm.Factor1" /><br/>
Factor2: <input type="number" ng-model="vm.Factor2" />
</div>
<script src="watch.js"></script>
</body>
</html>
watch.js
(
function ()
{
var app = angular.module('app', []);
function Controller(scope)
{
var vm = this;
vm.Factor1 = 0;
vm.Factor2 = 0;
// One of the factors has changed - recompute
vm.Listener = function (newValue, oldValue)
{
document.getElementById("computed-value").innerHTML = vm.Factor1 * vm.Factor2;
}
// Set up watch on Factor1
scope.$watch(function ()
{
return vm.Factor1;
},
vm.Listener);
// Set up watch on Factor2
scope.$watch(function ()
{
return vm.Factor2;
},
vm.Listener);
}
app.controller("controller", ["$scope", Controller]);
}
)();
The "computed-value" element is meant to contain the product of two input values. It is outside the Angular app, so a manual watch is set up on the two input values to keep that element in sync.
$scope.$watch: $watch(get, listener, [objectEquality])
.
This method is called to set up a watch on a piece of model data.
The $apply function causes one or more digest cycles to take place. There is rarely any need to use it, since Angular handles the synchronization between the scenes. It is kind of the opposite of the $watch function. It is used for informing Angular that data in the model have changed and that it should therefore run a digest to consume the new values. Normally, that data model is updated by the user. But if a change is made programmatically, the $apply function can be employed. It is a thin wrapper of the $digest method that takes a function as the single argument. That function is then called, wrapped in an exception handler, after which the $digest is run.
$scope.$apply: $apply([expression])
.
This method is called to make Angular run a digest cycle.
I have tried to come up with a sound and realistic example of when you might need $watch or $apply. One scenario where using those functions are handy is that of integrating a non-Angular UI widget. Such a widget will typically expose a value property of some sort, which can be set programmatically and which the user can update using the UI. An example of this could be a datepicker control. Such a widget can be used in an Angular app with no problems, except you cannot set an Angular binding on that value property. That can be solved by "Angularizing" the control, wrapping it in a custom directive. For a practical example, I have chosen to wrap the jQueryUI slider control in a directive. That control is simple enough that the general strategy can be explained without you having to know about the jQueryUI library. Some of the properties that can be set on such a slider control are min, max and value properties. The ambition now is to make a custom directive that wraps the slider control and implement two-way data binding on those three properties. In other words, when the user drags the slider, the corresponding value in the data model should be updated. And when a value in the data model is set programmatically, this should cause the slider to update on the page.
For the UI I added a couple of slider controls, to set random "Age" and "Height" properties of the data model. I also added three input fields, just to enable me to update the MinAge, MaxAge and Age properties in the data model.
apply.html
<!doctype html>
<html>
<head>
<title>Slider</title>
<link rel="stylesheet" href="Scripts/jqueryui/jquery-ui.css">
<style> div[jq-slider] { width:200px; } </style>
<script src="Scripts/jquery-2.1.4.js"></script>
<script src="Scripts/jqueryui/jquery-ui.js"></script>
<script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script>
</head>
<body ng-app="app" ng-Controller="controller as vm">
Age: <div jq-slider min="vm.MinAge" max="vm.MaxAge" value="vm.Age"></div>
Age: <input ng-model="vm.Age" /><br />
Min: <input ng-model="vm.MinAge" /><br />
Max: <input ng-model="vm.MaxAge" /><br />
Height: <div jq-slider max="250" value="vm.Height"></div>
<script src="apply.js"></script>
</body>
</html>
The jQueryUI slider posts a "slide" event whenever the user drags the slider, updating the underlying value. The example below subscribes to that event in order to update the corresponding value in the data model of the controller. But updating the value is not enough, because Angular will not know it has been updated. To inform Angular that something has changed, the $apply function is then called. Conversely, when a value is modified programmatically, the corresponding value of the slider control should be updated. Therefore, watches are set up for those properties, which causes a listener function to be called in response to a data update. That listener function can then reconfigure the slider.
apply.js
(
function ()
{
var app = angular.module("app", []);
function SliderWrapperDirective()
{
return {
scope: {
min: "=",
max: "=",
value: "=",
},
link: function (scope, element, attrs)
{
//// CONFIGURE SLIDER WITH $APPLY ////
var config = {
slide: function (event, ui)
{
scope.$apply(function ()
{
scope.value = ui.value;
});
}
};
function setConfigValue(name, value)
{
var int = parseInt(value);
if (!isNaN(int))
config[name] = int;
}
setConfigValue("value", scope.value);
setConfigValue("min", scope.min);
setConfigValue("max", scope.max);
element.slider(config);
//// SET UP WATCHES ////
function getMin()
{
return scope.min;
}
function getMax()
{
return scope.max;
}
function getValue()
{
return scope.value;
}
function setSliderValue(name, newValue)
{
var int = parseInt(newValue)
if (!isNaN(int)) {
var config = {};
config[name] = int;
element.slider(config);
}
}
scope.$watch(getMin, function (newValue, oldValue)
{
setSliderValue("min", newValue);
});
scope.$watch(getMax, function (newValue, oldValue)
{
setSliderValue("max", newValue);
});
scope.$watch(getValue, function (newValue, oldValue)
{
setSliderValue("value", newValue);
});
}
};
}
app.directive("jqSlider", SliderWrapperDirective);
function Controller()
{
var vm = this;
vm.Age = 17;
vm.MinAge = 0;
vm.MaxAge = 140;
vm.Height = 187;
}
app.controller("controller", [Controller]);
}
)();
I had to wrap the code that sets properties on the slider in a few functions, because that slider is not happy with non-numeric values such as NaN or undefined. The directive is generic enough that it now supports any jQueryUI slider instance with two-way binding on the min, max and value properties.
< Previous chapterNext : Communicating between components >
2016 by Niels Hede Pedersen