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>
Seriously, don't do it.

$compile cycle in directives

  1. A directive is found and $compile  is called
  2. The directive object's preLink  function is called
  3. The linking function returned from $compile
     is called
  4. 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