#▶Dave Smith - Deep Dive into Custom Directives - NG-Conf 2014 ★★★ ##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>
*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
- Why are they in separate module? To make them independatly unit testable. (Testing them without loading a whole app)
##Your First Directive How about a directive that extends a text to automatically highlight its text when the user focuses it?
<input type="text" select-all-on-focus >
angular.module('MyDirective').
directive('selectAllOnFocus', function(){
return {
restrict: 'A',
link: function(scope, element){
element.mouseup(function(event){
event.preventDefault();
});
element.focus(function(){
element.select();
});
}
}});
Note: The link() function arguments are positional, not injectable.
##Restrict ###Attributes only:
restrict: 'A'
< div my-directive> </div>
Whent to use: Behavioral HTML, like ng-click ###Elements only: restrict: 'E' <my-directive> </my-directive> Whent to use: Component HTML, like a custom widget ###Weird ones: (rarely used in practice) 'C' for classes and 'M' for comments
You can combine them: 'AE' ##Templates Two options for specifying the template:
templateDefine the HTML in JStemplateUrlUse 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"> facebook </a>
<a href="y"> twitter </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="Search text"></my-searchbox>
- directive has Isolate scope
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>'
}
})
##Isolate scope By default, a directive does not have its own scope.(e.g. 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
###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 direcrive 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)">
###attrs : Other way to pass data to directives
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 Js object:
{foo:1, bar: 42}
Use attrs.$set() to change HTML attrivute values.
Use $attrs.observe() to be notified when HTML attrivutes change. (Independant of digest loop, litlle bit faster)
##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. ###Like in hanglbars Handlebars uses similar concept to Angular's:
var template = Handlebars.compile('<div> ... </div>');
var html = template({ foo: 1, bar: 2});
In this contect, template is a lot like a link function.
The difference
- Handlebars' link function returns a string
- Angular's link function creates DOM at a scpecified location
###When should I use it? Directives that use compile: ng-repeat, ng-if, ng-switch
- When you need to add/remove DOM element after link time
- When you need to reuse a template multiple times
###Lazy loading expensive DOM ##Transclusion Think of transcludable directives as a picture frame. Frames html, and alows the caller to specify content. Tell Angular where to put the caller's content with ng-transclude. Inserts it as a child.
directive('myDialog', function(){
return {
restriction: 'E',
transclude: true,
template:
'<div class="modal">' +
' <div class="modal-body" ng-transclude></div'+
'</div'>
}
})
#▶The Power of $q by Dave Smith at ng-europe 2014 ##The old way Passing callbacks as function arguments
function getCurrentUser(callback){
$http.get('/api/user/me')
.success(function(user){ callback(user); })
}
Adding another request
function getPremission(callback){
$http.get('/api/premissions')
.success(function(premissions){ callback(premissions); })
}
Calling both...
getCurrentUser(function(user){ /*Do something with user*/ });
getPremissions(function(premission){ /*Do something with premissions*/ });
//Do something with user and premisions... How?
Serial?
getCurrentUser(function(user){
getPremissions(function(premissions){
//Do somethiong with premissions and user
})
});
But id doen not scale. Problems:
- Not parallelizable
- Not composable
- Not dynamic
##With $q we can do better! Two simple steps:
- Stop passing callbacks as parameters
- Start returning promises
function getCurrentUser(){
var deferred = $q.defere();
$http.get('/api/users/me')
.success(function(user){
deferred.resolve(user);
});
return deferred.promise;
}
getCurrentUser().then(function(user){ //Do something with user });
Suddenly, all your async operations become:
- Parallelizable
- Composable
- Dynamic
###Parallelizable
$q.all([getCurrentUser(), getPremissions()])
.then(function(responses){
var user = responses[0];
var premissions = responses[1];
});
This can be even nicer...
function spread(func) {
return function(array)
{
func.apply(void 0, array);
}
}
$q.all([getCurrentUser(), getPremissions()])
.then(spread(function(user, premissions){ //Do something with user and premissions }));
###Composable They can be chained.
###Dynamic
var promises = [getCurrentUser()];
if(shouldGetPermissions){
promises.push(getPermissions());
}
$q.all(promises).then(function(){ /* All done */});
##Why this API separation?
deferred.resolve()
promise.then()
This forces a separation between notifier and receiver.
Notifier is entity of code that has access to the deferred object, that constructed it and is responsible for notifying callers when operation is complete.
Receiver is entity of code who is simply subscribing to these services by way of the promise.
Reciever does not have access to the notifier objects because they could accidentaly trigger the resolution before the operations is complete. The only entity that can trigger the resolution is the one that constructed the deferred object.
##What about errors?
The notifier can send errors like this: deferred.reject()
The reciever can recieve them like this:
promise.then(function(){
/*success*/
}, function(){
/*failure*/
});
##What about status?
The notifier can send progress updates like this: deferred.notify(...)
The receiver can receive them like this:
promise.then(function(){
/* success */
}, function(){
/* failure */
}, function(){
/* progress (called 0 ot more times) */
})
##Deferreds: One time use Once you resolve or reject a deferred object, it cannot be resolved or deferred again.
##But you can be late to the party You can connect .then() to a promise after its deferred has been resoved, and your callback will be called at the next digest loop (this happens with $q).
##Last but not least: $q.when() On abstract level: It's all about taking values of any kind wrapping them in promisses and returning them. What can be passed: other promisses, literal values, objects,...
myApp.factory('movies', function($q, $http){
var cachedMovies;
return{
getMovies: function(){
return $q.when(cachedMovies || helper());
};
}
function helper(){
var deferred = $q.defer();
$http.get('/movies').succcess(function(movies){
cachedMovies = movies;
deferred.resolve(movies);
})
return deferred.promise;
}
});
But this one contains a bug. Bug fix:
myApp.factory('movies', function($q, $http){
var cachedMovies, p;
return{
getMovies: function(){
return $q.when(cachedMovies || p || helper());
};
}
function helper(){
var deferred = $q.defer();
p = deferred.promise;
$http.get('/movies').succcess(function(movies){
cachedMovies = movies;
deferred.resolve(movies);
})
return deferred.promise;
}
});
Side note $http already does caching. Look at the "cache" argument.
##$q is Angular's Champagne
- All $http requests use $q
- Request interceptors
- REsponse interceptors
- $interval and $timeout
- ngAnimate
- ngModelController (async validators)
- $tamplateRequest
Why did Angular write $q? Why not use the original "Q"? Because $q is aware of the digest loop. Why we would want promises to wait until the next digest loop. Thats just to make sure that when they execute a dirty check will happen after your then() function gets called. Otherwise you would have to apply digest yourself.