Deep Dive
into
Custom Directives
Alternate Title:
I've always wanted to know what "transclusion" means
Dave Smith
Links for Later
AngularJS Developer Guide:
Some useful directive libraries:
AngularUI:
AngularStrap:
Bad Directive Joke
What did the directive say after getting cut out of its parents' will?
"I'm isolated"
Prerequisites
Familiarity with these Angular terms:
-
Scope
- Controller
- Dependency Injection
Angular's Built-in Directives
- ng-show
- ng-hide
- ng-click
- ng-class
- ng-style
- ng-switch
- ng-repeat
- ng-animate
- And many more...
Naming Your Directives
- Use a unique prefix
- Avoids clashing with others
- Easier for readers to identify
- Don't use "ng-" for your custom prefix
- Common convention: two letters
- The AngularUI project uses "ui-"
- At HireVue, we use "hv-"
- For this talk, I'm using "my-"
- 1296 prefixes ought to be enough for anyone ;-)
ng-show
Usage:
<div ng-show="showThis">...</div>
Code:
directive('ngShow', ['$animate', function($animate) {
return function(scope, element, attr) {
scope.$watch(attr.ngShow, function(value){
$animate[toBoolean(value) ? 'removeClass' : 'addClass'](element,
'ng-hide');
});
};
}]);
Naming convention:
HTML: ng-show
JavaScript: ngShow
JavaScript: ngShow
ng-class
Usage:
<div ng-class="myClass()">...</div>
Code: (modified slightly for brevity and clarity)
directive('ngClass', function() {
return {
restrict: 'A',
link: function(scope, element, attr) {
var oldVal;
scope.$watch(attr.ngClass, ngClassWatchAction, true);
attr.$observe('class', function(value) {
ngClassWatchAction(scope.$eval(attr.ngClass));
});
function ngClassWatchAction(newVal) {
if (selector === true || scope.$index % 2 === selector) {
var newClasses = flattenClasses(newVal || '');
if(!oldVal) {
attr.$addClass(newClasses);
} else if(!equals(newVal,oldVal)) {
attr.$updateClass(newClasses, flattenClasses(oldVal));
}
}
oldVal = copy(newVal);
}
function flattenClasses(classVal) {
// snipped for brevity
}
}
};
});
When to use directives?
- If you want a reusable HTML component
<my-widget>
- If you want reusable HTML behavior
<div ng-click="...">
- If you want to wrap a jQuery plugin
<div ui-date></div>
- Almost any time you need to interface with the DOM
Getting Started
Create a module for your directives:
angular.module('MyDirectives', []);
Load the module in your app:
angular.module('MyApp', ['MyDirectives']);
Register your directive:
angular.module('MyDirectives').
directive('myDirective', function() {
// TODO Make this do something
});
Use your directive in your HTML:
<div my-directive>...</div>
Your First Directive
How about a directive that extends a text <input> to automatically highlight its text when the user focuses it?
<input type="text" select-all-on-focus />
Your First Directive
angular.module('MyDirectives').
directive('selectAllOnFocus', function() {
return {
restrict: 'A', // more on this soon
link: function(scope, element) {
element.mouseup(function(event) {
event.preventDefault();
});
element.focus(function() {
element.select();
});
}}});
Note: The link() function arguments are positional, not injectable.
Let's use it
<input type="text" ng-model="name" select-all-on-focus />
<input type="text" ng-model="age" select-all-on-focus />
Quiz:
How many times will the link() function be called?
What about the directive() function?
Bonus points: What about the compile() function?
(not shown here)
Restrict
Attributes only:
restrict: 'A'
<div my-directive></div>
When to use: Behavioral HTML, like ng-click
Elements only:
restrict: 'E'
<my-directive></my-directive>
When to use: Component HTML, like a custom widget
Weird ones: (rarely used in practice)
'C' for classes and 'M' for comments
Combine them:
restrict: 'AE'
Simple Widget Example
Let's make a simple widget:
<my-social-buttons></my-social-buttons>
We want this to expand to:
<div>
<a href="x"><i class="fa fa-facebook"></i></a>
<a href="y"><i class="fa fa-twitter"></i></a>
<a href="z"><i class="fa fa-google-plus"></i></a>
</div>
<my-social-buttons></my-social-buttons>
<div>
<a href="x"><i class="fa fa-facebook"></i></a>
<a href="y"><i class="fa fa-twitter"></i></a>
<a href="z"><i class="fa fa-google-plus"></i></a>
</div>
Simple Widget Example
directive('mySocialButtons', function() {
return {
restrict: 'E',
template:
'<div>' +
' <a href="x"><i class="fa fa-facebook"></i></a>' +
' <a href="y"><i class="fa fa-twitter"></i></a>' +
' <a href="z"><i class="fa fa-google-plus"></i></a>' +
'</div>'
}
});
New Concept: Templates
Templates
Two options for specifying the template:
- template Define the HTML in JavaScript
- templateUrl Use a URL to an HTML partial
Tip: You can avoid the round trip to the server with:
<script type="text/ng-template"
id="/partials/my-social-buttons.html">
<div>
<a href="x"><i class="fa fa-facebook"></i></a>
<a href="y"><i class="fa fa-twitter"></i></a>
<a href="z"><i class="fa fa-google-plus"></i></a>
</div>
</script>
How Angular downloads templates:
$http.get(templateUrl, {cache: $templateCache})
Interactive Widget Example
Let's make a custom search box
<my-searchbox
search-text="theSearchText"
placeholder="Please search now"
></my-searchbox>
We want it to appear like this in the browser
Custom Widget Example
angular.module('MyDirectives').
directive('mySearchbox', function() {
return {
restrict: 'E',
scope: {
searchText: '=',
placeholder: '@',
usedLucky: '='
},
template:
'<div>' +
' <input type="text" ng-model="tempSearchText" />' +
' <button ng-click="searchClicked()">' +
' Search' +
' </button>' +
' <button ng-click="luckyClicked()">' +
' I\'m feeling lucky' +
' </button>' +
'</div>'
}
});
New concept: Isolate Scope
Isolate Scope
By default, a directive does not have its own scope.
Example: ng-click
But you can give your directive its own scope like this:
scope: {
someParameter: '='
}
Or like this:
scope: true
Important: It does not inherit from the enclosing scope.
Isolate Scope
Why can it not inherit from the enclosing scope?
- To keep a clean interface between the directive and the caller
- To prevent the directive from (easily) accessing the caller's scope
- To encourage directive reuse
- To discourage guilty knowledge about the directive's environment
- To make the directive testable in isolation
Scope Parameters
Each scope parameter can be passed through HTML attributes:
scope: {
param1: '=', // two-way binding (reference)
param2: '@', // one-way expression (top down)
param3: '&' // one-way behavior (bottom up)
}
Examples:
<div my-directive
param1="someVariable"
param2="My name is {{theName}}, and I am {{theAge+10}}."
param3="doSomething(theName)"
>
Finishing the Custom Widget
directive('mySearchbox', function() {
return {
restrict: 'E',
scope: {
searchText: '=',
placeholder: '@',
usedLucky: '='
},
template: /* snip */,
link: function(scope, element, attrs) {
scope.searchClicked = function() {
scope.searchText = scope.tempSearchText;
scope.usedLucky = false;
}
scope.luckyClicked = function() {
scope.searchText = scope.tempSearchText;
scope.usedLucky = true;
}
}
}
});
HTML Attributes
There are other ways to pass data to your directives...
attrs
The attrs object gives read/write access to HTML attributes:
directive('myDirective', function() {
return {
link: function (scope, element, attrs) {
var options = scope.$eval(attrs.myDirective)
Usage:
function MyController($scope) {
$scope.someVariable = 42
<div my-directive="{foo: 1, bar: someVariable}">
options will contain this JavaScript object:
{foo: 1, bar: 42}
directive('myDirective', function() {
return {
link: function (scope, element, attrs) {
var options = scope.$eval(attrs.myDirective)
function MyController($scope) {
$scope.someVariable = 42
<div my-directive="{foo: 1, bar: someVariable}">
{foo: 1, bar: 42}
attrs
Use attrs.$set() to change HTML attribute values
Use $attrs.observe() to be notified
when HTML attributes change
when HTML attributes change
Compile and Link
Compile
Convert an HTML string into an Angular template.
Link
Insert an Angular template into the DOM with a scope as context.
Compile and Link
Handlebars uses a similar concept to Angular's:
var template = Handlebars.compile('<div>...</div>');
var html = template({foo: 1, bar: 2});
In this context, template is a lot like a link function.
The difference:
Handlebars' link function returns a string.
Angular's link function creates DOM at a specified location.
Compile and Link
Directives that use compile:
-
ng-repeat
-
ng-if
- ng-switch
When should I use it?
- When you need to add/remove DOM elements after link time
- When you need to reuse a template multiple times
Compile Example
Lazy loading expensive DOM
<div my-lazy-load="readyToShow">
<!-- Expensive DOM Here -->
</div>
How to do this?
Compile Example
Lazy loading expensive DOM
directive('myLazyLoad', function() {
return {
transclude: 'element',
priority: 1200, // changed needed for 1.2
terminal: true,
restrict: 'A',
compile: function(element, attr, linker) {
return function (scope, iterStartElement, attr) {
var hasBeenShown = false;
var unwatchFn = scope.$watch(attr.myLazyLoad, function(value){
if (value && !hasBeenShown) {
hasBeenShown = true;
linker(scope, function (clone) {
iterStartElement.after(clone);
});
unwatchFn();
}
});
}
}
});
New concepts: terminal and priority
Transclusion
Think of transcludable directives as a picture frame
Transclusion
Lets the caller specify the inner content of your directive
Tell Angular where to put the caller's content with ng-transclude:
directive('myDialog', function() {
return { restrict: 'E', transclude: true, template: '<div class="modal">' + ' <div class="modal-body" ng-transclude></div>' + '</div>' } });
Transclusion
This:
<my-dialog>
<h1>Some Custom Title</h1>
<p>Some custom dialog content goes here</p>
</my-dialog>
Expands to this:
<div class="modal">
<div class="modal-body">
<h1>Some Custom Title</h1>
<p>Some custom dialog content goes here</p>
</div>
</div>
Built-in Directives that Transclude
- ng-repeat
- ng-switch
- ng-if
Stuff We Didn't Talk About
-
Multiple directives on the same element
- How directives drive scope creation
- transclude:'element'
- Element directives for standard attributes
- e.g., "placeholder"
Deep Dive into custom Directives
By djsmith
Deep Dive into custom Directives
- 14,261