DIRECTIVES
Oh god why
Follow the presentation on your favorite device @ http://bit.do/peled
(there will be code so it's easier up close)
I'm Roy Peled
- Designed and built a major app FW for Conduit in Angular
- Designing and building a large app for Folloze in Angular
- Moderator of AngularJS group in Facebook
- Working with Angular for 2+ years
Angular Modules
- angular-class-extender @ Github
- angular-nested-resource @ Github
AGENDA
Part 1
Part 2
- What's ng-model?
- ng-model-options
- Custom validations
- Custom inputs
- Delving inside directives
Directives are hard.
Forms
What's ng-model?
<form name="demoForm">
<div ng-class="{'has-error': !demoForm.email.$valid}">
<input type="email" ng-model="email" name="email" required>
<span ng-show="demoForm.email.$error.email">Please enter a valid email</span>
</div>
<div ng-class="{'has-error': !demoForm.pword.$valid}">
<input type="password" ng-model="pword" name="pword" required ng-minlength="8">
<span ng-show="demoForm.pword.$error.minlength">Password too short!</span>
</div>
<button type="submit">Register</button>
</form>
Supports:
number, url, email, required, ng-minlength, ng-maxlength, ng-pattern
Checking for validation
<form name="demoForm" ng-submit="register()">
... </form>
app.controller('MainCtrl', function($scope) {
$scope.register = function(){
if($scope.demoForm.$valid)
$scope.loggedin = true;
}
});
ng-model-options
<input type="email" ng-model="email"
ng-model-options="{updateOn: 'blur'}">
<input type="password" ng-model="password"
ng-model-options="{updateOn: 'default', debounce: {'default':500}}">
Configuration that delays the update of the model
updateOn
A space separated array of triggers
debounce
Integer or object that defines the delay for each trigger
Custom validation?
- Should be easy to re-use (ie. directive)
- Should prevent form submission if invalid
- Should be lightweight
Introducing - the no-dogs directive
<input type="email" ng-model="email" name="mail" no-dogs required>
...
<span ng-show="demoForm.mail.$error.noDogs">No dogs allowed!</span>
Custom Validation
app.directive('noDogs', function factory(){
return {
require: '?ngModel',
link: function linking(scope, element, attr, ctrl){
if(ctrl){
ctrl.$parsers.push(function(value){
if(/dog/.test(value)){
ctrl.$setValidity("noDogs", false);
return undefined;
} else {
ctrl.$setValidity("noDogs", true);
return value;
}
});
}
}
}
});
ngModelController API
- $render() - Overwrite - Render the model
- $isEmpty(value) - Overwrite - Called by ng-model to determine if a value is empty
- $setValidity(validationError, isValid) - Use this method to validate the value
- $setViewValue(value, trigger) - Set the value from the DOM to ng-model, trigger for ngModelOptions
-
- $parsers - Array of function that format the value from the DOM, used for validation
- $formatters - Array of functions that format the value from the model
- $viewChangeListeners - Array of handlers that are fired when the view is changed
-
Custom Inputs
- Working with ng-model best practices
- Black box design pattern
- Simple to use and re-use
Creating a custom image selection drop down
(with the courtesy of bootstrap)
Custom input
<item-selector list="answers" ng-model="selection" name="ans" required />
<span ng-show="demoForm.ans.$error.required">
Hint - It's salty
</span>
<div class="btn-group item-selector">
...
<ul class="dropdown-menu">
...
<li ng-repeat="item in list">
<a href="" ng-click="select(item)">
<img src="{{item}}">
</a>
</li>
</ul>
</div>
app.directive('itemSelector', function factory(){
return {
templateUrl: "itemSelectorTemplate.html",
replace: true,
restrict: "E",
require: 'ngModel',
scope: {
list: "=",
value: "=ngModel"
},
link: function linking(scope, element, attr, ctrl){
scope.tries = 5;
scope.select = function(item){
ctrl.$setViewValue(item);
}
ctrl.$viewChangeListeners.push(function(){
scope.tries --;
})
}
}
});
But still...
Not extensible enough
ng-transclude
<item-selector list="answers" ng-model="selection" name="ans" required>
<a href="" ng-click="select(item)">
<img src="{{item}}">
</a>
</item-selector>
<span ng-show="demoForm.ans.$error.required">Hint - It's salty</span>
Take dynamic (runtime) template
into
static (configuration) template
<div class="btn-group item-selector">
...
<ul class="dropdown-menu" role="menu">
...
<li ng-repeat="item in list" ng-transclude></li>
</ul>
</div>
app.directive('itemSelector', function factory(){
return {
...
transclude: true,
...
link: function linking(scope, element, attr, ctrl){
...
}
}
});
<item-selector list="answers" ng-model="selection" name="ans" required>
<div class="button">
<img src="{{item}}" class="img-thumbnail">
<a href="" ng-click="select(item)" class="btn"> Select </a>
</div>
</item-selector>
<item-selector list="answers" ng-model="selection" name="ans" required>
<a href="" ng-click="select(item)" class="btn"> {{item}} </a>
</item-selector>
Simple, right?
But...
WE NEED TO GO DEEPER
So how do directives work?
var html = '<div ng-bind="exp"></div>';
var template = angular.element(html);
var linkFn = $compile(template);
var element = linkFn(scope);
parent.appendChild(element);
This a normal html page
First, angular wraps the requested node
The $compile collects all directives in the tree and returns an aggregated linking function.
The linking function attaches the scope to every directive and evaluates all watches.
Finally the element is added to the DOM.
Linking function?
That's the function responsible for Angular's 2-way data binding
DOM
<ul>
<li ng-repeat="item in list">
{{item.name}}
</li>
</ul>
Controller
$scope.list = getList();
$scope.$watch("list", function(){
...
});
⇦ Link function ⇨
What can we do with $compile?
You can basically override Angular's automated directives and create them manually. But that's redundant.
Here's how to do it:
var newScope = $rootScope.$new();
newScope.name = "Roy";
var linkFunc = $compile("<span>Hello {{name}}</span>");
var el = linkFunc(newScope);
console.log(el); // returns <span>Hello Roy</span>
$compile cycle in directives
- A directive is found and $compile
is called
- The directive object's preLink
function is called
-
The linking function returned from $compile
is called -
The directive object's postLink
function is called
myModule.directive('directiveName', function factory(injectables) {
return {
link: {
pre: function preLink(){...},
post: function postLink(){...)
}
}
});
For 99.9999% of the cases
we only need to use postLink
* when supplying link with a function instead of pre and post, the default will be postLink
What's inside postLink?
function postLink(scope, iElement, iAttrs, controller, transcludeFn){ ... }
- Scope - isolated or the relevant from the tree
- iElement - the compiled template after linking
- iAttrs - an object with all the attributes of the element
- controller - required controller
- transcludeFn - A transclude linking function
Tip
$parse is a getter for scope fields
$scope.name = { first: "Roy" }
<my-directive my-field="name.first"></my-directive>
myModule.directive('directiveName', function factory($parse) {
return {
link: function(scope, iElement, iAttrs){
var getter = $parse(iAttrs.myField);
// returns a getter for "name.first
var fieldValue = getter(scope);
// returns "Roy"
}
}
});
Transclude Function
When we want to manually control duplication of transcluded template
<div class="btn-group item-selector">
...
<ul class="dropdown-menu my-transclusion"></ul>
</div>
app.directive('itemSelector', function factory(){
return {
...
transclude: true,
...
link: function linking(scope, element, attr, ctrl, transcludeFn){
...
var trascludeContainer = getElement(".my-transclusion");
scope.$watch("list", function(list){
if(list){
angular.forEach(list, function(item){
var newScope = scope.$new();
newScope.item = item;
transcludeFn(newScope, function(element){
trascludeContainer.append(element);
})
});
}
})
}
}
});
Thank you
Roy Peled
- Presentation @ https://slides.com/roypeled
- Code @ http://plnkr.co/users/roypeled
- Git @ https://github.com/roypeled
- LinkedIn @ Roy Peled
- Facebook Group @ AngularJS
Q&A
Directives
By Roy Peled
Directives
- 4,841