The first thing anyone is taught when approaching Angular for the first time is that directives are meant to interact with the DOM, or whatever does DOM manipulation for you, such as jQuery (Get over it!). What immediately becomes (and stays) confusing for most, though, is the interaction between scopes, directives, and controllers. Particularly when we focus on scopes, and start factoring in the advanced concepts: the digest cycle, isolate scopes, transclusion, and the different linking functions in directives.
This (two-part) article aims to navigate the salt marsh that are Angular scopes and directives, while providing an amusingly informative, in-depth read. In the first part, this one, I’ll focus on scopes, and the life-cycle of an Angular application. The second part is focused on directives
The bar is high, but scopes are sufficiently hard to explain. If I’m going to fail miserably at it, at least I’ll throw in a few more promises I can’t keep!
If the following figure (source) looks unreasonably mind bending, then this article might be for you.
Disclaimer: article based on Angular v1.2.10 tree @ caed2dfe4f
.
Angular uses scopes to abstract communication between directives and the DOM. Scopes also exist in the controller level. Scopes are plain old JavaScript objects (POJO), which is fancy talk explaining that Angular does not heavily manipulate scopes, other than adding a bunch of properties, prefixed with one or two $
symbols. The ones prefixed with $$
aren’t necessary as frequently, and using them is often a code smell, which can be avoided by having a deeper understanding of the digest cycle.
What kind of scopes are we talking about?
In Angular slang, a “scope” is not what you might be used to, when thinking about JavaScript code, or even programming in general. Usually, scopes are used to refer to the bag in a piece of code which holds the context, variables, and so on.
In most languages variables are held in imaginary bags, which are defined by curly braces
{}
, or code blocks. This is known as block scoping. JavaScript, in contrast, deals in lexical scoping, which pretty much means the bags are defined by functions, or the global object, rather than code blocks.Bags can contain any number of smaller bags. Each bag can access the candy (sweet, sweet variables) inside its parent bag (and its parent’s parent, and so on), but they can’t poke holes in smaller, or child bags.
As a quick and dirty example, let’s examine the function below.
function eat (thing) {
console.log('Eating a ' + thing);
}
function nuts (peanut) {
var hazelnut = 'hazelnut';
function seeds () {
var almond = 'almond';
eat(hazelnut); // I can reach into the nuts bag!
}
// Inaccessible almond is inaccessible.
// Almonds are not nuts.
}
I won’t dwell on this
matter any longer, as these are not the scopes people refer to, when talking about Angular.
Scope inheritance in Angular.js
Scopes in Angular are also context, but on Angular terms. In Angular, a scope is associated to an element, while an element is not necessarily directly associated with a scope. Elements are assigned a scope is one of the following ways.
A scope is created on an element by a controller, or a directive (directives don’t always introduce new scopes).
<nav ng-controller='menuCtrl'>
If a scope isn’t present on the element, then it’s inherited from its parent.
<nav ng-controller='menuCtrl'>
<a ng-click='navigate()'>Click Me!</a> <!-- also <nav>'s scope -->
</nav>
If the element isn’t part of an ng-app
, then it doesn’t belong to an scope at all.
<head>
<h1>Pony Deli App</h1>
</head>
<main ng-app='PonyDeli'>
<nav ng-controller='menuCtrl'>
<a ng-click='navigate()'>Click Me!</a>
</nav>
</main>
To figure out an element’s scope, try to think of elements recursively inside-out following the three rules I’ve just outlined. Does it create a new scope? That’s its scope. Does it have a parent? Check the parent, then. Is it not part of an ng-app
? Tough luck, no scope.
You can (and most definitely should) use developer tools magic to easily figure out the scope for an element.
Examination of a telescopic sight
I’ll walk through a few properties in a typical scope as a way to introduce concepts, before moving on to explaining how digests work and behave internally. I’ll also let you in on how I’m getting to these properties. First, I’ll open Chrome and navigate to the application I’m working on, which is written in Angular. Then I’ll inspect on an element, and open the developer tools.
Did you know that
$0
gives you access to the last selected element in the Elements pane?$1
gives you access to the previously selected element, and so on.I prognosticate you’ll use
$0
the most, particularly when working with Angular.
For any given DOM element, angular.element
wraps that in either jQuery or jqLite, their little own mini-jQuery. Once wrapped, you get access to a scope()
function which returns, you guessed it, the Angular scope associated with that element. Combining that with $0
, I find myself using the following command quite often.
angular.element($0).scope()
Of course, if you just know you’re using jQuery, $($0).scope()
will work just the same. angular.element
works every time, regardless of jQuery.
Then I’m able to inspect the scope, assert that it’s the scope I expected, and whether the property values match what I was expecting, as well. Super useful. Let’s see what special properties are available on a typical scope.
for(o in $($0).scope())o[0]=='$'&&console.log(o)
That’s good enough, I’ll go over each property, clustering them by functionality, and going over each portion of Angular’s scoping philosophy.
Don’t buy a rifle scope without reading this
Here I’ve listed the properies yielded by that command, grouped by area of functionality. Let’s start with the basic ones, which merely provide scope navigation.
$id
Uniquely identifies the scope$root
Root scope$parent
Parent scope, ornull
ifscope == scope.$root
$$childHead
First child scope, if any; ornull
$$childTail
Last child scope, if any; ornull
$$prevSibling
Previous sibling scope, if any; ornull
$$nextSibling
Next sibling scope, if any; ornull
No surprises there. Navigating scopes like this would be utter non-sense. Sometimes accessing the $parent
scope might seem appropriate, but there are always better, less coupled, ways to deal with parental communication than tightly binding people-scopes together. One such way is using event listeners, our next batch of scope properties!
Events and partying: spreading the word
The properties described below let us publish events and subscribe to them. This is a pattern known as PubSub, or just events.
$$listeners
Event listeners registered on the scope$on(evt, fn)
Attaches an event listenerfn
namedevt
$emit(evt, args)
Fires eventevt
, roaring upward on the scope chain, triggering on the current scope and all$parent
s, including the$rootScope
$broadcast(evt, args)
Fires eventevt
, triggering on the current scope and all its children
When triggered, event listeners are passed an event
object, and any arguments passed to the $emit
or $broadcast
function. There are many ways in which scope events can provide value.
A directive might use events to announce something important happened. Check out this sample directive, where a button can be clicked to announce you feel like eating food of some type.
angular.module('PonyDeli').directive('food', function () {
return {
scope: { // I'll come back to directive scopes later
type: '=type'
},
template: '<button ng-click="eat()">I want to eat some {{type}}!</button>',
link: function (scope, element, attrs) {
scope.eat = function () {
letThemHaveIt();
scope.$emit('food.click', scope.type, element);
};
function letThemHaveIt () {
// do some fancy UI things
}
}
};
});
I like namespacing my events, and so should you. It avoids name collisions, and it’s clear where events originate from, or what event you’re subscribing to. Imagine you have an interest in analytics, and want to track clicks on food
elements using Mixpanel. That would actually be a reasonable need, and there’s no reason why that should be polluting your directive, or your controller. You could put together a directive which does the food-clicking analytics-tracking for you, in a nicely self-contained manner.
angular.module('PonyDeli').directive('foodTracker', function (mixpanelService) {
return {
link: function (scope, element, attrs) {
scope.$on('food.click', function (e, type) {
mixpanelService.track('food-eater', type);
});
}
};
});
The service implementation is not relevant here, as it would merely wrap Mixpanel’s client-side API. The HTML would look like below, and I threw in a controller, to hold all of the food types I want to serve in my deli. The ng-app
directive helps Angular to auto-bootstrap my application, as well. Rounding the example up, I added an ng-repeat
directive so I can render all of my food without repeating myself, it’ll just loop through foodTypes
, available on foodCtrl
's scope.
<ul ng-app='PonyDeli' ng-controller='foodCtrl' food-tracker>
<li food type='type' ng-repeat='type in foodTypes'></li>
</ul>
angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
$scope.foodTypes = ['onion', 'cucumber', 'hazelnut'];
});
The fully working example is hosted on CodePen.
That’s a good example on paper, but you need to think about whether you need an event anyone can subscribe to. Maybe a service will do? In this case, it could go either way. You could argue that you need events because you don’t know who else is going to subscribe to food.click
, and that means it’d be more “future-proof” to use events. You could also say that the food-tracker
directive doesn’t have a reason to be, as it doesn’t interact with the DOM or even the scope at all, other than to listen to an event which you could replace with a service.
Both thoughts would be correct, in the given context. As more components need to be food.click
-aware, it may feel clearer that events are the way to go. In reality, though, events are most useful when you actually need to bridge the gap between two scopes (or more), and other factors aren’t as important.
As we’ll see when we inspect directives more closely in the upcoming second part of this article, events aren’t even necessary for scopes to communicate. A child scope may read from its parent by binding to it, and it can also update those values.
There’s rarely a good reason to host events to help children communicate better with their parent.
Siblings often have a harder time communicating with each other, and they often do so through a parent they have in common. That generally translates into broadcasting from $rootScope
, and listening on the interested siblings, like below.
angular.module('PonyDeli').controller('foodCtrl', function ($rootScope) {
$scope.foodTypes = ['onion', 'cucumber', 'hazelnut'];
$scope.deliver = function (req) {
$rootScope.$broadcast('delivery.request', req);
};
});
angular.module('PonyDeli').controller('deliveryCtrl', function ($scope) {
$scope.$on('delivery.request', function (e, req) {
$scope.received = true; // deal with the request
});
});
<body ng-app='PonyDeli'>
<div ng-controller='foodCtrl'>
<ul food-tracker>
<li food type='type' ng-repeat='type in foodTypes'></li>
</ul>
<button ng-click='deliver()'>I want to eat that!</button>
</div>
<div ng-controller='deliveryCtrl'>
<span ng-show='received'>
A monkey has been dispatched. You shall eat soon.
</span>
</div>
</body>
This one is also on CodePen.
Over time you’ll learn to lean towards events or services accordingly. I could say that you should use events when you expect view models to change in response to event
, and you ought to use services otherwise, when you don’t expect view model changes. Sometimes the response is a mixture of both, where an action triggers an event which calls a service, or a service which broadcasts an event on $rootScope
. It depends on each situation, and you should analyze it as such, rather than attempting to nail down the elusive one-size-fits-all solution.
If you have two components which communicate through $rootScope
, you might prefer to use $rootScope.$emit
(rather than $broadcast
) and $rootScope.$on
. That way, the event will only spread among $rootScope.$$listeners
, and it won’t waste time looping through every children of $rootScope
, which you just know won’t have any listeners for that event. Here’s an example service using $rootScope
to provide events without limiting itself to a particular scope. It provides a subscribe method which allows consumers to register event listeners, and it might do things internally, which trigger that event.
angular.module('PonyDeli').factory("notificationService", function ($rootScope) {
function notify (data) {
$rootScope.$emit("notificationService.update", data);
}
function listen (fn) {
$rootScope.$on("notificationService.update", function (e, data) {
fn(data);
});
}
// anything that might have a reason
// to emit events at later points in time
function load () {
setInterval(notify.bind(null, 'Something happened!'), 1000);
}
return {
subscribe: listen,
load: load
};
});
You guessed right! This one is also on CodePen.
Enough events versus services banter, shall we move on to some other properties?
Digesting change-sets
Understanding this intimidating process is the cornerstone to understanding Angular.
Angular bases its data-binding features in a dirty-checking loop which tracks changes, and fires events when these change. This is simpler than it sounds. No, really. It is! Let me quickly go over each of the core components of the $digest
cycle. Firstly, there’s the scope.$digest
method. This method recursively digests changes in a scope and its children.
It should be noted that you need to be careful about triggering digests, because attempting to do so when you’re already in a digest phase will cause Angular to blow up in a mysterious haze of unexplainable phenomena. In other words, it’ll be pretty hard to pinpoint the root cause of the issue.
Let’s take a look at what the documentation has to say, regarding $digest
.
$digest()
Processes all of the watchers of the current scope and its children. Because a watcher’s listener can change the model, the $digest() keeps calling the watchers until no more listeners are firing. This means that it is possible to get into an infinite loop. This function will throw
'Maximum iteration limit exceeded.'
if the number of iterations exceeds 10.Usually, you don’t call $digest() directly in controllers or in directives. Instead, you should call $apply() (typically from within a directives), which will force a $digest().
So, a $digest
processes all watchers, and then the watchers those watchers trigger, until nothing else triggers a watch. There’s two questions left to understand this loop.
- What the hell is a watcher?
- Who triggers a
$digest
!?
Answering both of these questions can be made out to be as simple or as complicated as the person explaining them to you feels like. I’ll begin talking about watchers, and I’ll let you draw your own conclusions.
If you’ve read this far, you probably already know what a watcher is. You’ve probably used scope.$watch
, and maybe even used scope.$watchCollection
. The $$watchers
property has all the watchers on a scope.
$watch(watchExp, listener, objectEquality)
Adds a watch listener to the scope$watchCollection
Watches array items or object map properties$$watchers
Contains all the watches associated with the scope
Watchers are the single most important aspect of an Angular application’s data-binding capabilities, but Angular needs our help in order to trigger those watchers, because otherwise it can’t effectively update data-bound variables appropriately. Consider the following example.
angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
$scope.prop = 'initial value';
$scope.dependency = 'nothing yet!';
$scope.$watch('prop', function (value) {
$scope.dependency = 'prop is "' + value + '"! such amaze';
});
setTimeout(function () {
$scope.prop = 'something else';
}, 1000);
});
<body ng-app='PonyDeli'>
<ul ng-controller='foodCtrl'>
<li ng-bind='prop'></li>
<li ng-bind='dependency'></li>
</ul>
</body>
So you have 'initial value'
, and expect the second HTML line to change to 'prop is "something else"! such amaze'
after a second. Right? Even more interesting, you’d at the very least expect the first line to change to 'something else'
! Why doesn’t it? That’s not a watcher… or is it?
Actually, a lot of what you do in the HTML markup ends up creating a watcher. In this case, each ng-bind
directive created a watcher on the property. It will update the HTML of the <li>
, whenever prop
and dependency
change, similarly to how our watch will change the property itself.
That way, you can now think of your code as having three watches, one for each ng-bind
directive, and the one in the controller. How is Angular supposed to know the property is updated, after the timeout? You could tell it, just by adding a manual digest to the timeout callback.
setTimeout(function () {
$scope.prop = 'something else';
$scope.$digest();
}, 1000);
Here’s a CodePen without the $digest
, and one that does $digest
, after the timeout. The more Angular way to do it, however, would be using the $timeout
service instead of setTimeout
. It provides some error handling, and executes $apply()
.
$apply(expr)
Parses and evaluates an expression, then executes the digest loop on$rootScope
In addition to executing the digest on every scope, $apply
provides error handling functionality, as well. If you’re trying to tune your performance, then using $digest
may be warranted, but I’d stay away from it until you feel really comfortable with how Angular works internally.
We’re back to the second question, now.
- Who triggers a
$digest
!?
Digests are triggered internally in strategic places all over the Angular code-base. They are triggered either directly or by calls to $apply()
, like we’ve observed in the $timeout
service. Most directives, both those found in Angular core and those out in the wild, trigger digests. Digests fire your watchers, and watchers update your UI. That’s the basic idea, anyways.
There’s a pretty good resource with best practices in the Angular Wiki, which you can find linked at the bottom of this article.
A word of advice, regarding advice
Ever since I’ve come aboard the Angular boat, I’ve read lots of advice on how to structure your code, what to do; and what not to do, when working with Angular. The truth is that you need to take advice regarding Angular with a pinch of salt. There’s lots of bad advice clinging to the web, from back when Angular wasn’t the mature framework that it is today, or written by people who don’t have a clue what they’re talking about.
Even the good advice is one guy’s opinion, and you shouldn’t stick to whatever worked for someone else, just because they’ve blogged about them. I do believe that you should read about what other people believe to be best practices, and embrace them, if you feel they’re adequate. But don’t turn them into your unbreakable mantra, because they’ll break you.
I’ve explained how watches and the digest loop interact with each other. Below, I listed properties related to the digest loop, which you can find on a scope. These help you parse text expressions through Angular’s compiler, or execute pieces of code at different points of the digest cycle.
$eval(expression, locals)
Parse and evaluate an scope expression immediately$evalAsync(expression)
Parse and evaluate an expression at a later point in time$$asyncQueue
Async task queue, consumed on every digest$$postDigest(fn)
Executesfn
after the next digest cycle$$postDigestQueue
Methods registered with$$postDigest(fn)
Phew, that’s it. It wasn’t that bad, was it?
The Scope
is dead, long live the Scope
!
These are the last few, rather dull-looking, properties in a scope. They deal with the scope life cycle, and are mostly used for internal purposes, although there are cases where you may want to $new
scopes by yourself.
- $$isolateBindings Isolate scope bindings, e.g
{ options: '@megaOptions' }
. Very internal- $new(isolate) Creates a child scope, or an isolate scope, which won’t inherit from its parent
- $destroy Removes the scope from the scope chain. Scope and children won’t receive events, and watches won’t fire anymore
- $$destroyed Has the scope been destroyed?
Isolate scopes? What is this madness? The second part of this article is dedicated to directives, and it covers isolate scopes, transclusion, linking functions, compilers, directive controllers, and more.
Further Reading
Here’s some additional resources you can read to further extend your comprehension of Angular.
- The Angular Way
- Anti Patterns
- Best Practices
- TodoMVC Angular.js Example
- Training Videos from John Lindquist
- ng-newsletter
- Using scope.$watch and scope.$apply
- Part 2: Angle Brackets, Synergistic Directives
Please comment on any issues regarding this article, so everyone can benefit from your feedback. Also, you should follow me on Twitter!
Comments