What exactly is a scope in Angular? From the name, you might guess that it is a context for application state, possibly provided to protect us from using JavaScript's much-maligned global scope. That sounds like a simple, prudent thing for a framework to create, perhaps something we shouldn't even think about. Can we just move on to the next chapter?
Not so fast. Although this will not be a long chapter, it does cover something very important: scope inheritance and hierarchy. A typical Angular application might create a hierarchy of dozens, hundreds, or even thousands of scopes.
Before we get started, let's set up the environment for this chapter's examples.
In the Basics chapter, we learned how to add the ng-app
directive to an element in order to tell Angular which part of your document it should process.
<body ng-app="app">
<!-- Other examples to be inserted here. -->
</body>
The argument to ng-app
is the name of our application's root module. (The example module name, app
, is just a convention.) Angular modules will be covered in depth in an upcoming chapter. For now, just consider this some bootstrapping boilerplate that you can ignore.
angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
$controllerProvider.allowGlobals();
}]);
Now that we have that out of the way, we can get on with business.
In the last chapter, Controllers, we learned how to prepare the model by attaching properties to a $scope
reference. Let's repeat the exercise.
function NameController($scope) {
$scope.name = "First";
}
Using the ng-controller
directive, we can invoke the controller function above in the context of a DOM element. Any data that we assign to the scope provided to this controller will be available to us on and within this p
element.
<p ng-controller="NameController">
{{name}}
</p>
DOM elements that are outside of the element on which we have chosen to invoke NameController
will not have access to the scope created for this usage of the controller. Let's test that this is true.
<div>
<p>
Outside the scope: {{name}}
</p>
<div ng-controller="NameController">
<p>
Inside the scope: {{name}}
</p>
</div>
</div>
Now that we have seen how scopes are paired with controllers, let's look at the opposite case: an Angular application with just one scope.
Angular is always trying its best to make sure we stay out of trouble when using scopes, so it creates a new one for every controller. However, scopes are hierarchical, and at the root of the scope hierarchy for every application is a single ancestor. We access this single root scope simply by declaring the specially-named $rootScope
parameter in our controllers, and using it instead of the normal (and recommended!) $scope
reference.
function RootNameController($rootScope) {
$rootScope.name = "First";
}
This looks innocent enough, and indeed it works just fine.
<p ng-controller="RootNameController">
{{name}}
</p>
However, trouble arises when we attempt to assign a property with the same name in another controller.
function SecondRootNameController($rootScope) {
$rootScope.name = "Second";
}
This is not good. The $rootScope
is a singleton in our application, and can only have one name
property.
<p ng-controller="RootNameController">
{{name}}
</p>
<p ng-controller="SecondRootNameController">
{{name}}
</p>
And indeed, our SecondRootNameController
has replaced the value that RootNameController
set on name
. Well, that is the problem with globals, isn't it?
By automatically giving each controller its own scope, Angular provides a significantly safer environment for us. Let's rewrite the controllers to publish the model the correct way, using $scope
rather than $rootScope
. Keep in mind that I only showed you the $rootScope
examples above to illustrate why Angular creates a new scope object for each controller.
function SecondNameController($scope) {
$scope.name = "Second";
}
Using the NameController
that was shown at the beginning of this chapter, and the SecondNameController
above, we can demonstrate the isolation of the scope objects that are passed to controllers declaring $scope
.
<p ng-controller="NameController">
{{name}}
</p>
<p ng-controller="SecondNameController">
{{name}}
</p>
This example produces the correct output, with a correct name
value for each controller. This isolation is the behavior you get when neither controller is a child of the other, meaning neither is declared on a DOM element enclosed by that of the other controller. What will happen if we nest them instead?
Modifying the last example slightly, we can move SecondNameController
to a child element of the div
that loads NameController
, in order to produce a situation in which the controllers are nested.
<div ng-controller="NameController">
<p>
{{name}}
</p>
<p ng-controller="SecondNameController">
{{name}}
</p>
</div>
Things still work correctly, in that the name
properties are isolated from each other. What if you reverse the order of the p
elements? Go ahead, give it a try. You should still see both name
values in the output, just reversed.
It would seem that the models of nested controllers remain isolated, but this is misleading. In fact, Angular organizes controllers into a hierarchy based on their relative position in the DOM, and a nested controller inherits the properties of its ancestors. The reason we do not perceive a change is that the name
property in the child scope shadows the property of the same name in the parent scope.
Let's see if we can coax out evidence of this behavior by changing the name of the property set on the child scope.
function ChildController($scope) {
$scope.childName = "Child";
}
We'll render the two properties in both the parent and the child scopes.
<div ng-controller="NameController">
<p>
{{name}} and {{childName}}
</p>
<p ng-controller="ChildController">
{{name}} and {{childName}}
</p>
</div>
This example makes it clear that NameController
does not have access to the properties of its child, while ChildController
has access to both its own properties and those of its parent.
Since name
is an inherited property, it would make sense that we should see changes to it updated in both the parent and child scopes. Let's add an input that is bound to name
on the parent.
<div ng-controller="NameController">
<p>
{{name}}
</p>
<p ng-controller="ChildController">
{{name}}
</p>
<input type='text' ng-model='name'>
</div>
If you try it editing name
above, you'll see that this works as expected. The name
property is updated in the context of both scopes. But again, please note that this is working with an input that is bound to name
on the parent scope.
It would also make sense that we should be able to change name
on the child scope and see the change reflected on the parent. At least, you would think so, right? Should we try? Let's add a text box that lets us modify the name
property on the child scope.
In the rendered view of the example, carefully do the following: First, change the value in the upper text input. You will see the value of name
updated everywhere. Second, change the value in the lower text input.
<div ng-controller="NameController">
<p>
name: {{name}}
<br>
<input type='text' ng-model='name'>
</p>
<p ng-controller="ChildController">
name: {{name}}
<br>
<input type='text' ng-model='name'>
</p>
</div>
Are you surprised by the result?
Angular uses JavaScript's ordinary prototypal inheritance, which is partly good, because if you understand prototypal inheritance, you do not need to learn anything new. However, it is partly bad, because JavaScript prototypal inheritance is not entirely intuitive.
Setting a property on a JavaScript object results in the creation of that property on the object. This simple rule is bad news for inherited properties, which are shadowed by the object's own properties.
Huh? Put simply, there was no name
property on the child until you modified the text in the lower input. Once you did that, Angular assigned the value to name
in the child and the property was created. Once created, it blocked upward access to name
on the parent.
Got it? If not, take a moment to study a bit more about prototypal inheritance. If so, let's continue.
How do we deal with this in Angular, so that we can modify model data on an inherited scope?
It's really easy. We just need to move the name
property to another object.
function InfoController($scope) {
$scope.info = {name: "First"};
}
function ChildInfoController($scope) {
$scope.info.childName = "Child";
}
<div ng-controller="InfoController">
<p>
{{info.name}} and {{info.childName}}
<br>
<input type='text' ng-model='info.name'>
</p>
<p ng-controller="ChildInfoController">
{{info.name}} and {{info.childName}}
<br>
<input type='text' ng-model='info.name'>
</p>
</div>
Notice that ChildInfoController
depends on its parent to create the info
object. What happens if you edit the source code for ChildInfoController
, replacing the function body with this statement: $scope.info = {childName: "Second"};
. Try it. You'll see that we are back to creating properties on the child, with the shadowing effect seen earlier.
Most of the time, Angular's two-way binding interacts with properties on the scope as you would expect: When you use a bound input to make a change, the UI is updated everywhere. However, computed properties, meaning scope data derived from other scope data, are another story. In the example below, sum
is a computed property.
function SumController($scope) {
$scope.values = [1,2];
$scope.newValue = 1;
$scope.add = function() {
$scope.values.push(parseInt($scope.newValue));
};
// Broken -- doesn't trigger UI update
$scope.sum = $scope.values.reduce(function(a, b) {
return a + b;
});
}
The last statement in SumController
is where sum
is actually computed, in a simple operation using the reduce
function.
In the template for this example, below, we use a select
input to let the user choose a number (either 1
, 2
, or 3
) to add to the end of the values
array. (As an aside, notice that the controller provides an initial value for newValue
. If it didn't, Angular would add a blank option to the select
elements, in order to avoid arbitrarily setting newValue
to the first option generated from the comprehension in ng-options
. This behavior has nothing to do with scopes, but is useful to know.)
<p ng-controller="SumController">
<select ng-model="newValue" ng-options="n for n in [1,2,3]"></select>
<input type="button" value="Add" ng-click="add()">
The sum of {{values}} is {{sum}}.
</p>
Clicking Add
should result in a change to the displayed value for sum
, but sadly, it doesn't. Try it for yourself by selecting a value and clicking the Add
button.
Let's fix things by moving the statement that computes the value of sum
into a callback function. By passing this callback function as an argument to $scope.$watch
along with a watchExpression
argument (in this case, just the name of the property from which sum
is computed), we arrange for sum to be recomputed whenever values
is changed.
function SumController($scope) {
$scope.values = [1,2];
$scope.newValue = 1;
$scope.add = function() {
$scope.values.push(parseInt($scope.newValue));
};
$scope.$watch('values', function () {
$scope.sum = $scope.values.reduce(function(a, b) {
return a + b;
});
}, true);
}
And sure enough, the displayed value of sum
is now dynamically updated in the view.
Example of two-way binding by passing two functions (getter and setter) to $watch
Angular's built-in directives for two-way bindings are full-featured for sure, but every once in a while you will find some behavior that you need to add. For example, what if we want the user to be able to clear both the current state of a text input and its bound state on the scope with the esc
key? How can we code this custom event handling?
<div ng-controller="EscapeController">
<input type="text" ng-model="message">
is bound to
"<strong ng-bind="message"></strong>".
Press <code>esc</code> to clear it!
</div>
First, we must declare the specially-named $element
parameter on our controller, so that Angular injects a reference to the controller's associated DOM element. Using the bind
function on the provided element, we register a callback for the keyup
event in which we check for the esc
key. In this callback we update the scope property. Easy enough, right? Give it a try. Type something, then press the esc
key.
function EscapeController($scope, $element) {
$scope.message = '';
$element.bind('keyup', function (event) {
if (event.keyCode === 27) { // esc key
// Broken -- doesn't trigger UI update
$scope.message = '';
}
});
}
Not quite. Since we're working (almost) directly with the DOM here, we need to let Angular know when it's time to repaint the view. We do this by wrapping our changes to the scope in a callback passed to $scope.$apply.
function EscapeController($scope, $element) {
$scope.message = '';
$element.bind('keyup', function (event) {
if (event.keyCode === 27) { // esc key
$scope.$apply(function() {
$scope.message = '';
});
}
});
}
Try it again in the rewritten example. Now that Angular knows what we're up to, things work perfectly.
If you're trying to fit Angular into the Model-view-controller (MVC) paradigm, scopes pose a bit of a conundrum. It starts out easy enough: Scopes are clearly part of the model layer. In Angular, an object isn't a model until it is reachable as a property of a scope. But the story gets more interesting when you look at how scopes are bound to the DOM via controllers or directives. Fortunately, our academic questions aside, scopes are intuitive and easy to use, as the examples in this chapter have shown.