AngularJS’ Internals In Depth, Part 2
In the previous article in this series, I discussed scope events and the behavior of the digest cycle. This time around, I’ll talk about directives. This article will cover isolate scopes, transclusion, linking functions, compilers, directive controllers and more.
If the figure looks unreasonably mind-bending, then this article might be for you.
Disclaimer: This article is based on the AngularJS v1.3.0 tree.
What The Hell Is A Directive?
A directive is a typically small component that is meant to interact with the DOM in AngularJS. It is used as an abstraction layer on top of the DOM, and most manipulation can be achieved without touching DOM elements, wrapped in jQuery, jqLite or otherwise. This is accomplished by using expressions, and other directives, to achieve the results you want.
Directives in AngularJS’ core can bind an element’s property (such as visibility, class list, inner text, inner HTML, or value) to a scope’s property or expression. Most notably, these bindings will be updated whenever changes in the scope are digested, using watches. Similarly, and in the opposite direction, DOM attributes can we “watched” using an $observe
function, which will trigger a callback whenever the watched property changes.
Directives are, simply put, the single most important face of AngularJS. If you master directives, then you won’t have any issues dealing with AngularJS applications. Likewise, if you don’t manage to get a hold of directives, you’ll be cluelessly grasping at straws, unsure what you’ll pull off next. Mastering directives takes time, particularly if you’re trying to stay away from merely wrapping a snippet of jQuery-powered code and calling it a day.
In AngularJS, you’re able to build componentized directives, services and controllers that can be reused as often as it makes sense for them to be reused. For instance, you might have a simple directive that turns on a class based on a watched scope expression, and I’d imagine that would be a pretty common directive, used everywhere in your application, to signal the state of a particular component in your code. You could have a service to aggregate keyboard shortcut-handling, and have controllers, directives and other services register shortcuts with that service, rooting all of your keyboard shortcut-handling in one nicely self-contained service.
Directives are also reusable pieces of functionality, but most often, these are assigned to DOM fragments, or templates, rather than merely providing functionality. Time to dive deep down into AngularJS directives and their use cases.
Creating A Directive
Earlier, I listed each property available on a scope in AngularJS, and I used that to explain the digest mechanism and how scopes operate. I’ll do the same for directives, but this time I’ll go through the properties of the object returned by a directive’s factory function and how each of those properties influences the directive we’re defining.
The first thing of note is the name of the directive. Let’s look at a brief example.
angular.module('PonyDeli').directive('pieceOfFood', function () {
var definition = { //
Even though in the snippet above we’re defining a directive named ‘pieceOfFood’
, AngularJS convention stipulates that we use a hyphenated version of that name in the HTML markup. That is, if this directive were implemented as an attribute, then I might need to refer it in my HTML like so:
<span piece-of-food></span>
By default, directives can only be triggered as attributes. But what if you want to change this behavior? You can use the restrict
option.
restrict
Defines how a directive may be applied in markup
angular.module('PonyDeli').directive('pieceOfFood', function () {
return {
restrict: 'E',
template: // ...
};
});
For some reason I cannot fathom, they’ve decided to obfuscate what’s otherwise a verbose framework, and we’ve ended up with single capital letters to define how a directive is restricted. A list of available restrict
choices appears on GitHub, and the default value is EA
.
'A'
: attributes are allowed<span piece-of-food></span>
'E'
: elements are allowed<piece-of-food></piece-of-food>
'C'
: as a class name<span class='piece-of-food'></span>
'M'
: as a comment<!-- directive: piece-of-food -->
'AE'
: You can combine any of these to loosen up the restriction a bit.
Don’t ever use ‘C’
or ’M’
to restrict your directives. Using ‘C’
doesn’t stand out in markup, and ’M’
was meant for backwards compatibility. If you feel like being funny, though, you could make a case for setting restrict
to ‘ACME’
.
(Remember how in the last article I said to take advice with a pinch of salt? Don’t do that with mine — my advice is awesome!)
Unfortunately, the rest of the properties in a directive definition object are much more obscure.
scope
sets how a directive interacts with the$parent
scope
Because we discussed scopes at length in the previous article, learning how to use the scope
property properly shouldn’t be all that excruciating. Let’s start with the default value, scope: false
, where the scope chain remains unaffected: You’ll get whatever scope is found on the associated element, following the rules I outlined in the previous article.
Leaving the scope chain untouched is obviously useful when your directive doesn’t interact with the scope at all, but that rarely happens. A much more common scenario in which not touching the scope is useful is creating a directive that has no reason to be instanced more than once on any given scope and that just interacts with a single scope property, the directive’s name. This is most declarative when combined with restrict: ‘A’
, the default restrict
value. (The code below is available on Codepen.)
angular.module('PonyDeli').directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = value;
});
}
};
});
<body ng-app='PonyDeli'>
<span piece-of-food='Fish & Chips'></span>
</body>
There are a few things to note here that we haven’t discussed yet. You’ll learn more about the link
property later in this article. For the time being, think of it as a controller that runs for each instance of the directive.
In the directive’s linking function, we can access attrs
, which is a collection of attributes present on element
. This collection has a special method, called $observe()
, which will fire a callback whenever a property changes. Without watching the attribute for changes, the property wouldn’t ever make it to the scope, and we wouldn’t be able to bind to it in our template.
We can twist the code above, making it much more useful, by adding scope.$eval
to the mix. Remember how it can be used to evaluate an expression against a scope? Look at the code below (also on Codepen) to get a better idea of how that could help us.
var deli = angular.module('PonyDeli', []);
deli.controller('foodCtrl', function ($scope) {
$scope.piece = 'Fish & Chips';
});
deli.directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = scope.$eval(value);
});
}
};
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<span piece-of-food='piece'></span>
</body>
In this case, I’m evaluating the attribute’s value, piece
, against the scope, which defined $scope.piece
at the controller. Of course, you could use a template like {{piece}}
directly, but that would require specific knowledge about which property in the scope you want to track. This pattern provides a little more flexibility, although you’re still going to be sharing the scope across all directives, which can lead to unexpected behavior if you were to try adding more than one directive in the same scope.
Playful Child Scopes
You could solve that issue by creating a child scope, which inherits prototypically from its parent. To create a child scope, you merely need to declare scope: true
.
var deli = angular.module('PonyDeli', []);
deli.controller('foodCtrl', function ($scope) {
$scope.pieces = ['Fish & Chips', 'Potato Salad'];
});
deli.directive('pieceOfFood', function () {
return {
template: '{{pieceOfFood}}',
scope: true,
link: function (scope, element, attrs) {
attrs.$observe('pieceOfFood', function (value) {
scope.pieceOfFood = scope.$eval(value);
});
}
};
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p piece-of-food='pieces[0]'></p>
<p piece-of-food='pieces[1]'></p>
</body>
As you can see, we’re now able to use multiple instances of the directive and get the desired behavior because each directive is creating its own scope. However, there’s a limitation: Multiple directives on an element all get the same scope.
Note: If multiple directives on the same element request a new scope, then only one new scope is created.
Lonely, Isolate Scope
One last option is to create a local, or isolate, scope. The difference between an isolate scope and a child scope is that the former doesn’t inherit from its parent (but it’s still accessible on scope.$parent
). You can declare an isolate scope like this: scope: {}
. You can add properties to the object, which get data-bound to the parent scope but are accessible on the local scope. Much like restrict
, isolate scope properties have a terse but confusing syntax, in which you can use symbols such as &
, @
and =
to define how the property is bound.
You may omit the property’s name if you’re going to use that as the key in your local scope. That is to say, pieceOfFood: ‘=’
is a shorthand for pieceOfFood: ‘=pieceOfFood’
; they are equivalent.
Choose Your Weapon: @
, &
Or =
What do those symbols mean, then? The examples I coded, enumerated below, might help you decode them.
Attribute Observer: @
Using @
binds to the result of observing an attribute against the parent scope.
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p note='You just bought some {{type}}'></p>
</body>
deli.directive('note', function () {
return {
template: '{{note}}',
scope: {
note: '@'
}
};
});
This is equivalent to observing the attribute for changes and updating our local scope. Of course, using the @
notation is much more “AngularJS.”
deli.directive('note', function () {
return {
template: '{{note}}',
scope: {},
link: function (scope, element, attrs) {
attrs.$observe('note', function (value) {
scope.note = value;
});
}
};
});
Attribute observers are most useful when consuming options for a directive. If we want to change the directive’s behavior based on changing options, though, then writing the attrs.$observe
line ourselves might make more sense than having AngularJS do it internally and creating a watch on our end, which would be slower.
In these cases, merely replacing scope.note = value
, in the $observe
handler shown above, into whatever you would have put on the $watch
listener should do.
Note: keep in mind that, when dealing with @
, we’re talking about observing and attribute, instead of binding to the parent scope.
Expression Builder: &
Using &
gives you an expression-evaluating function in the context of the parent scope.
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<p note='"You just bought some " + type'></p>
</body>
deli.directive('note', function () {
return {
template: '{{note()}}',
scope: {
note: '&'
}
};
});
Below, I’ve outlined how you might implement that same functionality in the linking function, in case you aren’t aware of &
. This one is a tad lengthier than @
, because it is parsing the expression in the attribute once, building a reusable function.
deli.directive('note', function ($parse) {
return {
template: '{{note()}}',
scope: {},
link: function (scope, element, attrs) {
var parentGet = $parse(attrs.note);
scope.note = function (locals) {
return parentGet(scope.$parent, locals);
};
}
};
});
Expression builders, as we can see, generate a method that queries the parent scope. You can execute the method whenever you’d like and even watch it for output changes. This method should be treated as a read-only query on a parent expression and, as such, would be most useful in two scenarios. The first one is when you need to watch for changes on the parent scope, in which case you would set up a watch on the function expression note()
, which is, in essence, what we did in the example above.
The other situation in which this might come in handy is when you need access to a method on the parent scope. Suppose the parent scope has a method that refreshes a table, while your local scope represents a table row. When the table row is deleted, you might want to refresh the table. If the button is in the child scope, then it would make sense to use a &
binding to access the refresh functionality on the parent scope. That’s just a contrived example — you might prefer to use events for that kind of thing, or maybe even structure your application in some way so that complicating things like that can be avoided.
Bi-Directional Binding: =
Using =
sets up bi-directional binding between the local and parent scopes.
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<button countable='clicks'></button>
<span>Got {{clicks}} clicks!</span>
</body>
deli.directive('countable', function () {
return {
template:
'<button ng-disabled="!remaining">' +
'Click me {{remaining}} more times! ({{count}})' +
'</button>',
replace: true,
scope: {
count: '=countable'
},
link: function (scope, element, attrs) {
scope.remaining = 10;
element.bind('click', function () {
scope.remaining--;
scope.count++;
scope.$apply();
});
}
};
});
Bi-directional binding is quite a bit more complicated than &
or @
.
deli.directive('countable', function ($parse) {
return {
template:
'<button ng-disabled="!remaining">' +
'Click me {{remaining}} more times! ({{count}})' +
'</button>',
replace: true,
scope: {},
link: function (scope, element, attrs) {
// you're definitely better off just using '&'
var compare;
var parentGet = $parse(attrs.countable);
if (parentGet.literal) {
compare = angular.equals;
} else {
compare = function(a,b) { return a === b; };
}
var parentSet = parentGet.assign; // or throw
var lastValue = scope.count = parentGet(scope.$parent);
scope.$watch(function () {
var value = parentGet(scope.$parent);
if (!compare(value, scope.count)) {
if (!compare(value, lastValue)) {
scope.count = value;
} else {
parentSet(scope.$parent, value = scope.count);
}
}
return lastValue = value;
}, null, parentGet.literal);
// I told you!
scope.remaining = 10;
element.bind('click', function () {
scope.remaining--;
scope.count++;
scope.$apply();
});
}
};
});
This form of data-binding is arguably the most useful of all three. In this case, the parent scope property is kept in sync with the local scope. Whenever the local scope’s value is updated, it gets set on the parent scope. Likewise, whenever the parent scope value changes, the local scope gets updated. The most straightforward scenario I’ve got for you for when this would be useful is whenever you have a child scope that is used to represent a sub-model of the parent scope. Think of your typical CRUD table (create, read, update, delete). The table as a whole would be the parent scope, whereas each row would be contained in an isolate directive that binds to the row’s data model through a two-way =
binding. This would allow for modularity, while still allowing for effective communication between the master table and its children.
That took a lot of words, but I think I’ve managed to sum up how the scope
property works when declaring directives and what the most common use cases are. Let’s move on to other properties in the directive definition object, shall we?
Sensible View Templates
Directives are most effective when they contain small reusable snippets of HTML. That’s where the true power of directives comes from. These templates may be provided in plain text or as a resource that AngularJS queries when bootstrapping the directive.
template
This is how you would provide the view template as plain text.template: '<span ng-bind="message" />'
templateUrl
This allows you to provide the URL to an HTML template.templateUrl: /partials/message.html
Using templateUrl
to separate the HTML from your linking function is awesome. Making an AJAX request whenever you want to initialize a directive for the first time, not so much. However, you can circumvent the AJAX request if you pre-fill the $templateCache
with a build task, such as grunt-angular-templates. You could also inline your view templates in the HTML, but that is slower because the DOM has to be parsed, and that’s not as convenient in a large project with a ton of views. You don’t want an immense “layout” with all of the things, but rather individual files that contain just the one view. That would be the best of both worlds: separation of concerns without the extra overhead of AJAX calls.
You could also provide a function (tElement, tAttrs)
as the template
, but this is neither necessary nor useful.
replace
Should the template be inserted as a child element or inlined?
The documentation for this property is woefully confusing:
"replace
specify where the template should be inserted. Defaults tofalse
.
true
— the template will replace the current elementfalse
— the template will replace the contents of the current element"
So, when replace is false
, the directive actually replaces the element? That doesn’t sound right. If you check out my pen, you’ll find out that the element simply gets appended if replace: false
, and it gets sort of replaced if replace: true
.
As a rule of thumb, try to keep replacements to a minimum. Directives should keep interference with the DOM as close as possible to none, whenever possible, of course.
Directives are compiled, which results in a pre-linking function and a post-linking function. You can define the code that returns these functions or just provide them. Below are the different ways you can provide linking functions. I warn you: This is yet another one of those “features” in AngularJS that I feel is more of a drawback, because it confuses the hell out of newcomers for little to no gain. Behold!
compile: function (templateElement, templateAttrs) {
return {
pre: function (scope, instanceElement, instanceAttrs, controller) {
// pre-linking function
},
post: function (scope, instanceElement, instanceAttrs, controller) {
// post-linking function
}
}
}
compile: function (templateElement, templateAttrs) {
return function (scope, instanceElement, instanceAttrs, controller) {
// post-linking function
};
}
link: {
pre: function (scope, instanceElement, instanceAttrs, controller) {
// pre-linking function
},
post: function (scope, instanceElement, instanceAttrs, controller) {
// post-linking function
}
}
link: function (scope, instanceElement, instanceAttrs, controller) {
// post-linking function
}
Actually, you could even forget about the directive definition object we’ve been discussing thus far and merely return a post-linking function. However, this isn’t recommended even by AngularJS peeps, so you’d better stay away from it. Note that the linking functions don’t follow the dependency-injection model you find when declaring controllers or directives. For the most part, dependency injection in AngularJS is made available at the top level of the API, but most other methods have static well-documented parameter lists that you can’t change.
deli.directive('food', function () {
return function (scope, element, attrs) {
// post-linking function
};
});
Before proceeding, here’s an important note from the AngularJS documentation I’d like you to take a look at:
Note: The template instance and the link instance may be different objects if the template has been cloned. For this reason it is not safe to do anything other than DOM transformations that apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration should be done in a linking function rather than in a compile function.
Compile functions currently take in a third parameter, a transclude linking function, but it’s deprecated. Also, you shouldn’t be altering the DOM during compile functions (on templateElement
). Just do yourself a favor and avoid compile
entirely; provide pre-linking and post-linking functions directly. Most often, a post-linking function is just enough, which is what you’re using when you assign a link
function to the definition object.
I have a rule for you here. Always use a post-linking function. If a scope absolutely needs to be pre-populated before the DOM is linked, then do just that in the pre-linking function, but bind the functionality in the post-linking function, like you normally would have. You’ll rarely need to do this, but I think it’s still worth mentioning.
link: {
pre: function (scope, element, attrs, controller) {
scope.requiredThing = [1, 2, 3];
},
post: function (scope, element, attrs, controller) {
scope.squeal = function () {
scope.$emit("squeal");
};
}
}
controller
This is a controller instance on the directive.
Directives can have controllers, which makes sense because directives can create a scope. The controller is shared among all directives on the scope, and it is accessible as the fourth argument in linking functions. These controllers are a useful communication channel across directives on the same scoping level, which may be contained in the directive itself.
controllerAs
This is the controller alias to refer to in the template.
Using a controller alias allows you to use the controller within the template itself, because it will be made available in the scope.
require
This will throw an error if you don’t link some other directive(s) on this element!
The documentation for require
is surprisingly straightforward, so I’ll just cheat and paste that here:
"Require another directive and inject its controller as the fourth argument to the linking function. Therequire
takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the injected argument will be an array in corresponding order. If no such directive can be found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with:
(no prefix)
Locate the required controller on the current element. Throw an error if not found?
Attempt to locate the required controller or passnull
to thelink
fn if not found^
Locate the required controller by searching the element’s parents. Throw an error if not found?^
Attempt to locate the required controller by searching the element’s parents or passnull
to thelink
fn if not found"
Require is useful when our directive depends on other directives in order to work. For example, you might have a dropdown directive that depends on a list-view directive, or an error dialog directive that depends on having an error message directive. The example below, on the other hand, defines a needs-model
directive that throws an error if it doesn’t find an accompanying ng-model
— presumably because needs-model
uses that directive or somehow depends on it being available on the element.
angular.module('PonyDeli').directive(‘needsModel’, function () {
return {
require: 'ngModel’,
}
});
<div needs-model ng-model=’foo’></div>
priority
This defines the order in which directives are applied.
Cheating time!
"When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied. Thepriority
is used to sort the directives before theircompile
functions get called. Priority is defined as a number. Directives with greater numericalpriority
are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order. The order of directives with the same priority is undefined. The default priority is0
."
terminal
This prevents further processing of directives.
"If set to true then the currentpriority
will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on samepriority
is undefined)."
Transcluding For Much Win
transclude
This compiles the content of the element and makes it available to the directive.
I’ve saved the best (worst?) for last. This property allows two values, for more fun and less profit. You can set it either to true
, which enables transclusion, or to ‘element’
, in which case the whole element, including any directives defined at a lower priority, get transcluded.
At a high level, transclusion allows the consumer of a directive to define a snippet of HTML, which can then be included into some part of the directive, using an ng-transclude
directive. This sounds way too complicated, and it’s only kind of complicated. An example might make things clearer.
angular.module('PonyDeli').directive('transclusion', function () {
return {
restrict: 'E',
template:
'<div ng-hide="hidden" class="transcluded">' +
'<span ng-transclude></span>' +
'<span ng-click="hidden=true" class="close">Close</span>' +
'</div>',
transclude: true
};
});
<body ng-app='PonyDeli'>
<transclusion>
<span>The plot thickens!</span>
</transclusion>
</body>
You can check it out on CodePen, of course. What happens when you try to get scopes into the mix? Well, the content that gets transcluded inside the directive will still respond to the parent content, correctly, even though it’s placed inside the directive and even if the directive presents an isolate scope. This is what you’d expect because the transcluded content is defined in the consuming code, which belongs to the parent scope, and not the directive’s scope. The directive still binds to its local scope, as usual.
var deli = angular.module('PonyDeli', []);
deli.controller('foodCtrl', function ($scope) {
$scope.message = 'The plot thickens!';
});
deli.directive('transclusion', function () {
return {
restrict: 'E',
template:
'<div ng-hide="hidden" class="transcluded">' +
'<span ng-transclude></span>' +
'<span ng-click="hidden=true" class="close" ng-bind="close"></span>' +
'</div>',
transclude: true,
scope: {},
link: function (scope) {
scope.close = 'Close';
}
};
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
<transclusion>
<span ng-bind='message'></span>
</transclusion>
</body>
You can find that one on CodePen as well. There you have it: transclusion, demystified.
Other Resources
Here are some additional resources you can read to extend your comprehension of AngularJS.
- “AngularJS’ Internals in Depth, Part 1,” Nicolas Bevacqua, Smashing Magazine
- “AngularJS : When writing a directive, how do I decide if a need no new scope, a new child scope, or a new isolate scope?,” StackOverflow
- “Transclusion Basics” (screencast), John Lindquist, Egghead.io
- “AngularJS : When to use transclude ‘true’ and transclude ‘element’?,” StackOverflow
- “Understanding AngularJS Directives Part 1: Ng-repeat and Compile,” Liam Kaufman
Please comment on any issues regarding this article, so that everyone can benefit from your feedback. Also, you should follow me on Twitter!
Further Reading
- An Introduction To Unit Testing In AngularJS Applications
- Why You Should Consider React Native For Your Mobile App
- Automating Style Guide-Driven Development
- Getting Started With Neon Branching