文章目录
  1. 1. What Are Directives
  2. 2. ng-include
    1. 2.1. Limitations of ng-include
  3. 3. ng-switch
  4. 4. Creating a Directive
    1. 4.1. Restrict
    2. 4.2. The link Function
    3. 4.3. Scope
    4. 4.4. Replace
  5. 5. Unit Testing Dirctives
  6. 6. AngularJS Life Cycle
    1. 6.1. The Digest Cycle
    2. 6.2. Directive Life Cycle
  7. 7. Transclusions
    1. 7.1. Advanced Transclusion
  8. 8. Directive Controllers and require
  9. 9. require Options
  10. 10. Input Directives with ng-model
  11. 11. Custom Validators
  12. 12. Compile
    1. 12.1. Pre- and Post- Linking
  13. 13. Priority and Terminal
  14. 14. Clean Up and Destroy
  15. 15. Watchers
  16. 16. $apply and $digest

What Are Directives

Directives are of two major types in AngularJS

  • Behavior modifiers: These types of directives work on existing UI and HTML snippets, and just add or modify the existing behavior of what the UI does.
  • Reusable components: These types of directives are the more common variety, in which the directive cre‐ ates a whole new HTML structure.

ng-include

The ng-include directive takes an AngularJS expression and treats its value as the path to an HTML file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!-- File: chapter11/ng-include/stock.html -->
<div class="stock-dash">
  Name:
  <span class="stock-name"
        ng-bind="stock.name">
  </span>
  Price:
  <span class="stock-price"
        ng-bind="stock.price | currency">
  </span>
  Percentage Change:
  <span class="stock-change"
        ng-bind="mainCtrl.getChange(stock) + '%'">
  </span>
</div>

<body ng-app="stockMarketApp">
  <div ng-controller="MainCtrl as mainCtrl">
  <h3>List of Stocks</h3>
  <div ng-repeat="stock in mainCtrl.stocks">
    <div ng-include="mainCtrl.stockTemplate">
    </div>
  </div>
</div>
<script>
// File: chapter11/ng-include/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
  var self = this;
  self.stocks = [
    {name: 'First Stock', price: 100, previous: 220},
    {name: 'Second Stock', price: 140, previous: 120},
    {name: 'Third Stock', price: 110, previous: 110},
    {name: 'Fourth Stock', price: 400, previous: 420}
  ];
  self.stockTemplate = 'stock.html';
  self.getChange = function(stock) {
    return Math.ceil((
      (stock.price - stock.previous) / stock.previous) * 100);
  };
}]);

</script>

We can also do it like this <div ng-include="'views/stock.html'"></div>

Limitations of ng-include

Although we changed the name of the variable in the main index.html file, the stock.html file still expects a variable calledb stock for it to display.

ng-switch

The ng-switch is another directive that allows us to add some functionality to the UI for selectively displaying certain snippets of HTML. It gives us a way of conditionally including HTML snippets by behaving like a switch case directly in the HTML.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div ng-controller="MainCtrl as mainCtrl">
  <h3>Conditional Elements in HTML</h3>
  <button ng-click="mainCtrl.currentTab = 'tab1'">
    Tab 1
  </button>
  <button ng-click="mainCtrl.currentTab = 'tab2'">
    Tab 2
  </button>
  <button ng-click="mainCtrl.currentTab = 'tab3'">
    Tab 3
  </button>
  <button ng-click="mainCtrl.currentTab = 'something'">
    Trigger Default
  </button>
  <div ng-switch="mainCtrl.currentTab">
    <div ng-switch-when="tab1">
      Tab 1 is selected
    </div>
    <div ng-switch-when="tab2">
      Tab 2 is selected
    </div>
    <div ng-switch-when="tab3">
      Tab 3 is selected
    </div>
    <div ng-switch-default>
      No known tab selected
    </div>
  </div>
</div>

ng-switch-when does not understand AngularJS expressions.

Creating a Directive

AngularJS converts dashes to camelCase. Thus, stock-widget (or STOCK-WIDGET or even Stock-Widget) in HTML becomes stockWidget in JavaScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div ng-controller="MainCtrl as mainCtrl">
  <h3>List of Stocks</h3>
  <div ng-repeat="stock in mainCtrl.stocks">
    <div stock-widget> </div>
  </div>
</div>
<script>
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html' // template: <...>
  };
}]);

</script>

Restrict

The restrict keyword defines how someone using the directive in their code might use it. The default way of using directives is via attributes of existing element

  • A, The letter A in the value for restrict specifies that the directive can be used as an attribute on existing HTML elements (such as <div stock-widget></div>).
  • E, The letter E in the value for restrict specifies that the directive can be used as a new HTML element (such as <stock-widget></stock-widget>).
  • C, The letter C in the value for restrict specifies that the directive can be used as a class name in existing HTML elements (such as <div class="stock-widget"></div>).
  • M, The letter M in the value for restrict specifies that the directive can be used as HTML comments
1
2
3
4
5
6
7
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html',
    restrict: 'AE'
  };
}]);

Best Practice

  • Internet Explorer 8 and below do not like custom HTML elements. AngularJS, with version 1.3 onwards, has dropped support for (or rather, testing on) Internet Explorer 8.
  • Class-based directives are ideal for rendering-related work, like the ng-cloak
  • Element directives are recommended if we are creating entirely new HTML content.
  • Attribute directives are usually preferred for behavior modifiers. (like ng-show, ng-class, and so on)

The link function does for a directive what a controller does for a view—it defines APIs and functions that are necessary for the directive, in addition to manipulating and working with the DOM.

AngularJS executes the link function for each instance of the directive, so each instance can get its own, fully contained business logic while not affecting any other instance of the directive. link: function($scope, $element, $attr){}

If we need to add functionality to our instance of the directive, we can add it to the scope of the element we’re working with.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// File: chapter11/directive-with-link/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html',
    restrict: 'AE',
    link: function($scope, $element, $attrs) {
      $scope.getChange = function(stock) {
      return Math.ceil(((stock.price - stock.previous) /
        stock.previous) * 100);
      };
    }
  };
}]);

Scope

By default, each directive inherits its parent’s scope, which is passed to it in the link function. This can lead to the following problems:

  • Adding variables/functions to the scope modifies the parent as well, which suddenly gets access to more variables and functions.
  • The directive might unintentionally override an existing function or variable with the same name.
  • The directive can implicitly start using variables and functions from the parent. This might cause issues if we start renaming properties in the parent and forget to do it in the directive.

AngularJS gives us the scope key in the directive definition object to have complete control over the scope of the directive element.

  • false This is the default value, which basically tells AngularJS that the directive scope is the same as the parent scope, whichever one it is. So the directive gets access to all the variables and functions that are defined on the parent scope, and any modifications it makes are immediately reflected in the parent as well.
  • true This tells AngularJS that the directive scope inherits the parent scope, but creates a child scope of its own. The directive thus gets access to all the variables and functions from the parent scope.
  • object We can also pass an object with keys and values to the scope. This tells AngularJS to create what we call an isolated scope.

In particular, we can specify three types of values that can be passed in, which AngularJS will directly put on the scope of the directive:

  • =, the value of the attribute in HTML to be treated as a JSON object, which will be bound to the scope of the directive so that any changes done in the parent scope will be automatically available in the directive.
  • @, the value of the attribute in HTML is to be treated as a string, which may or may not have AngularJS binding expressions ({\{ }}).
  • &, the value of the attribute in HTML is a function in some controller whose reference needs to be available to the directive.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<div ng-controller="MainCtrl as mainCtrl">
  <h3>List of Stocks</h3>
    <div ng-repeat="s in mainCtrl.stocks">
      <div stock-widget stock-data="s">
    </div>
  </div>
</div>
<script>
// File: chapter11/directive-with-scope/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html',
    restrict: 'A',
    scope: {
      stockData: '='
    },
    link: function($scope, $element, $attrs) {
      $scope.getChange = function(stock) {
        return Math.ceil(((stock.price - stock.previous) /
        stock.previous) * 100);
      };
    }
  };
}]);
</script>
<!-- File: chapter11/directive-with-scope/stock.html -->
<div class="stock-dash">
  Name:
  <span class="stock-name"
        ng-bind="stockData.name">
  </span>
  Price:
  <span class="stock-price"
        ng-bind="stockData.price | currency">
  </span>
  Percentage Change:
  <span class="stock-change"
        ng-bind="getChange(stockData) + '%'">
  </span>
</div>

Replace

For such cases, AngularJS offers the replace key as part of the directive definition object. The replace key takes a Boolean, and it defaults to false. If we specify it to true, AngularJS removes the element that the directive is declared on, and replaces it with the HTML template from the directive definition object.

With AngularJS version 1.3 forward, the replace keyword in the directive definition object has been deprecated.

Unit Testing Dirctives

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// File: chapter12/stockDirective.js
angular.module('stockMarketApp', [])
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html',
    restrict: 'A',
    scope: {
      stockData: '=',
      stockTitle: '@',
      whenSelect: '&'
    },
    link: function($scope, $element, $attrs) {
      $scope.getChange = function(stock) {
      return Math.ceil(((stock.price - stock.previous) /
        stock.previous) * 100);
      };
      $scope.onSelect = function() {
      $scope.whenSelect({
        stockName: $scope.stockData.name,
        stockPrice: $scope.stockData.price,
        stockPrevious: $scope.stockData.previous
      });
    };
  }
};
}]);
  1. Get the $compile service injected into our test.
  2. Set up our directive instance HTML.
  3. Create and set up our scope with the necessary variables.
  4. Determine the template to load because our server is mocked out.
  5. Instantiate an instance of our directive using the $compile service.
  6. Write our expectations for rendering and behavior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// File: chapter12/stockDirectiveRenderSpec.js
describe('Stock Widget Directive Rendering', function() {
  beforeEach(module('stockMarketApp'));
  var compile, mockBackend, rootScope;
  // Step 1
  beforeEach(inject(function($compile, $httpBackend, $rootScope) {
    compile = $compile;
    mockBackend = $httpBackend;
    rootScope = $rootScope;
  }));
  it('should render HTML based on scope correctly', function() {
    // Step 2
    var scope = rootScope.$new();
    scope.myStock = {
      name: 'Best Stock',
      price: 100,
      previous: 200
    };
    scope.title = 'the best';
    // Step 3
    mockBackend.expectGET('stock.html').respond(
      '<div ng-bind="stockTitle"></div>' +
      '<div ng-bind="stockData.price"></div>');
    // Step 4
    var element = compile('<div stock-widget' +
      ' stock-data="myStock"' +
      ' stock-title="This is {\{title}}"></div>')(scope);
    // Step 5
    scope.$digest();
    mockBackend.flush();
    // Step 6
    expect(element.html()).toEqual(
      '<div ng-bind="stockTitle" class="ng-binding">' +
      'This is the best' +
      '</div>' +
      '<div ng-bind="stockData.price" class="ng-binding">' +
      '100' +
      '</div>');


    // Step 6
    var compiledElementScope = element.isolateScope();
    expect(compiledElementScope.stockData)
      .toEqual(scope.myStock);
    expect(compiledElementScope.getChange(
      compiledElementScope.stockData)).toEqual(-50);
    // Step 7
    expect(scopeClickCalled).toEqual('');
    compiledElementScope.onSelect();
    expect(scopeClickCalled).toEqual('100;200;Best Stock');
  });
});

AngularJS Life Cycle

When an AngularJS application is loaded in our browser window, the following events are executed in order:

  1. The HTML page is loaded:
    • The HTML page loads the AngularJS source code (with jQuery optionally loaded before).
    • The HTML page loads the application’s JavaScript code.
  2. The HTML page finishes loading.
  3. When the document ready event is fired, AngularJS bootstraps and searches for any and all instances of the ng-app attribute in the HTML:
  4. Within the context of each (there could be more than one) ng-app, AngularJS starts its magic by running the HTML content inside ng-app through what is known as the compile step:
    • The compile step goes through each line of HTML and looks for AngularJS directives.
    • For each directive, it executes the necessary code as defined by that directive’s definition object.
    • At the end of the compile step, a link function is generated for each directive that has a handle on all the elements and attributes that need to be controlled by AngularJS.
  5. AngularJS takes the link function and combines it with scope.
  6. At the end of this, we have a live, interactive view with the content filled in for the user.

AngularJS adds watchers for all its bindings and ng-model. And whenever one of the aforementioned events happens, AngularJS checks its watchers and bindings to see if anything has changed.

The Digest Cycle

The digest cycle in AngularJS is responsible for keeping the UI up to date in an AngularJS application. The AngularJS UI update cycle happens as follows:

  1. When the application loads, or when any HTML is loaded within AngularJS, AngularJS runs its compilations step, and keeps track of all the watchers and listeners that are needed for the HTML
  2. AngularJS also keeps track of all the elements that are bound to the HTML for each scope.
  3. When one of the events mentioned in the previous section happens, AngularJS triggers the digest cycle.
  4. In the digest cycle, AngularJS starts from $rootScope and checks each watcher in the scope to see if the current value differs from the value it’s displaying in the UI.
  5. If nothing has changed, it recurses to all the parent scopes and so on until all the scopes are verified.
  6. If AngularJS finds a watcher at any scope that reports a change in state, AngularJS stops right there, and reruns the digest cycle.
  7. The digest cycle is rerun because a change in a watcher might have an implication on a watcher that was already evaluated beforehand. To ensure that no data change is missed, the digest cycle is rerun.
  8. AngularJS reruns the digest cycle every time it encounters a change until the digest cycle stabilizes.
  9. When the digest stabilizes, AngularJS accumulates all the UI updates and triggers them at once.

Directive Life Cycle

  1. When the application loads, the directive definition object is triggered. This hap‐ pens only once.
  2. Next, when the directive is encountered in the HTML the very first time, the tem‐ plate for the directive is loaded.
  3. This template is then compiled and AngularJS handles the other directives present in the HTML. This generates a link function that can be used to link the directive to a scope.
  4. The scope for the directive instance is created or acquired. This could be the parent scope, a child of the parent scope, or an isolated scope as the case might be
  5. The link function (and the controller) execute for the directive.

Transclusions

AngularJS directives have a concept of transclusions to allow us to create reusable directives where each implementation might need to render a certain section of the UI differently.

  1. First, we tell the directive that we are going to use transclusion as part of this di‐ rective.
  2. Second, we need to tell AngularJS where to put the content that was stored in the template.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
// File: chapter13/directive-transclusion/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
  return {
    templateUrl: 'stock.html',
    restrict: 'A',
    transclude: true,
    scope: {
      stockData: '='
    },
    link: function($scope, $element, $attrs) {
      $scope.getChange = function(stock) {
        return Math.ceil(((stock.price - stock.previous) /
          stock.previous) * 100);
      };
    }
  };
}]);
</script>

<!-- File: chapter13/directive-transclusion/stock.html -->
<div class="stock-dash">
  <span ng-transclude></span>
  Price:
  <span class="stock-price"
        ng-bind="stockData.price | currency">
  </span>
  Percentage Change:
  <span class="stock-change"
        ng-bind="getChange(stockData) + '%'">
  </span>
</div>

The ng-transclude content explicitly refers to something that is available in the scope of ng-repeat, but not inside the directive’s scope.

Thus, the transcluded content and the directive content form a sibling relationship but do not share the same scope.

Advanced Transclusion

We try to create a trivial replacement for the ng-repeat that will pick up some variables from our outer scope, and add some variables for each instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<script>
// File: chapter13/directive-advanced-transclusion/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
  var self = this;
  self.stocks = [
    {name: 'First Stock', price: 100, previous: 220},
    {name: 'Second Stock', price: 140, previous: 120},
    {name: 'Third Stock', price: 110, previous: 110},
    {name: 'Fourth Stock', price: 400, previous: 420}
  ];
}]);


// File: chapter13/directive-advanced-transclusion/directive.js
angular.module('stockMarketApp').directive('simpleStockRepeat',
  [function() {
    return {
      restrict: 'A',
      // Capture and replace the entire element
      // instead of just its content
      transclude: 'element',
      // A $transclude is passed in as the fifth
      // argument to the link function
      link: function($scope, $element, $attrs, ctrl, $transclude) {
        var myArray = $scope.$eval($attrs.simpleStockRepeat);
        var container = angular.element(
          '<div class="container"></div>');
        for (var i = 0; i < myArray.length; i++) {
          // Create an element instance with a new child
          // scope using the clone linking function
          var instance = $transclude($scope.$new(),
            function(clonedElement, newScope) {
              // Expose custom variables for the instance
              newScope.currentIndex = i;
              newScope.stock = myArray[i];
            });
          // Add it to our container
          container.append(instance);
        }
        // With transclude: 'element', the element gets replaced
        // with a comment. Add our generated content
        // after the comment
        $element.after(container);
      }
    };
}]);
</script>

<div ng-controller="MainCtrl as mainCtrl">
  <h3>List of Stocks</h3>
  <div simple-stock-repeat="mainCtrl.stocks">
    We found {\{stock.name}} at {\{currentIndex}}
  </div>
</div>

Because transclude element copies the entire element, it also removes the element from the HTML

Directive Controllers and require

Directive controllers are used in AngularJS for inter-directive communication, while link functions are fully contained and specific to the directive instance.

By inter-directive communication, we mean when one directive on an element wants to communicate with another directive on its parent or on the same element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<script>
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
  var self = this;
  self.startedTime = new Date().getTime();
  self.stocks = [
    {name: 'First Stock', price: 100, previous: 220},
    {name: 'Second Stock', price: 140, previous: 120},
    {name: 'Third Stock', price: 110, previous: 110},
    {name: 'Fourth Stock', price: 400, previous: 420}
  ];
}]);

// File: chapter13/directive-controllers/tabs.js
angular.module('stockMarketApp')
.directive('tabs', [function() {
  return {
    restrict: 'E',
    transclude: true,
    scope: true,
    template: '<div class="tab-headers">' +
      ' <div ng-repeat="tab in tabs"' +
      'ng-click="selectTab($index)"' +
      'ng-class="{selected: isSelectedTab($index)}">' +
      '<span ng-bind="tab.title"></span>' +
      ' </div>' +
      '</div>' +
      '<div ng-transclude></div> ',
    controller: function($scope) {
      var currentIndex = 0;
      $scope.tabs = [];
      this.registerTab = function(title, scope) {
        if ($scope.tabs.length === 0) {
          scope.selected = true;
        } else {
          scope.selected = false;
        }
        $scope.tabs.push({title: title, scope: scope});
      };
      $scope.selectTab = function(index) {
        currentIndex = index;
        for (var i = 0; i < $scope.tabs.length; i++) {
          $scope.tabs[i].scope.selected = currentIndex === i;
        }
      };
      $scope.isSelectedTab = function(index) {
        return currentIndex === index;
      };
    }
  };
}]);

// File: chapter13/directive-controllers/tab.js
angular.module('stockMarketApp')
.directive('tab', [function() {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div ng-show="selected" ng-transclude></div>',
    require: '^tabs',
    scope: true,
    link: function($scope, $element, $attr, tabCtrl) {
      tabCtrl.registerTab($attr.title, $scope);
    }
  };
}]);
</script>

<div ng-controller="MainCtrl as mainCtrl">
  <tabs>
    <tab title="First Tab">
      This is the first tab.
      The app started at {\{mainCtrl.startedTime | date}}
    </tab>
    <tab title="Second Tab">
      This is the second tab
      <div ng-repeat="stock in mainCtrl.stocks">
           Stock Name: {\{stock.name}}
      </div>
    </tab>
  </tabs>
</div>

A directive controller is a function that gets the scope and element injected in.

The controller can define functions that are specific to the directive instance by defining them on $scope as we have been doing so far, and define the API or accessible functions and variables by defining them on this or the controller’s instance.

require Options

The require keyword in the directive definition object either takes a string or an array of strings, each of which is the name of the directive that must be used in conjunction with the current directive.

  • require: 'tabs',tells AngularJS to look for a directive called tabs, which exposes a controller on the same element the directive is on. Implies that AngularJS should locate the directive tabs on the same element, and throw an error if it’s not found:
  • require: ['tabs', 'ngModel'], tells AngularJS that both the tabs and ng-model directives must be present on the ele‐ ment our directive is used on. When used as an array, the link function gets an array of controllers as the fourth argument, instead of just one controller.
  • require: '?tabs' tells AngularJS to treat the directive as an optional dependency.
  • require: '^tabs' tells AngularJS that the tabs directive must be present on one of the parent elements.
  • require: '?^tabs'

Input Directives with ng-model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<div ng-controller="MainCtrl as mainCtrl">
  <div>
  The current value of the slider is 
  </div>
  <no-ui-slider class="slider"
                ng-model="mainCtrl.selectedValue"
                range-min="500"
                range-max="5000">
  </no-ui-slider>
  <div>
    <input type="number"
           ng-model="mainCtrl.textValue"
           min="500"
           max="5000"
           placeholder="Set a value">
    <button ng-click="mainCtrl.setSelectedValue()">
      Set slider value
    </button>
  </div>
</div>


<scirpt>
// File: chapter13/directive-slider/app.js
angular.module('sliderApp', [])
  .controller('MainCtrl', [function() {
    var self = this;
    self.selectedValue = 2000;
    self.textValue = 4000;
    self.setSelectedValue = function() {
    self.selectedValue = self.textValue;
  };
}]);



// File: chapter13/directive-slider/noui-slider.js
angular.module('sliderApp')
.directive('noUiSlider', [function() {
  return {
    restrict: 'E',
    require: 'ngModel',
    link: function($scope, $element, $attr, ngModelCtrl) {
      $element.noUiSlider({
        // We might not have the initial value in ngModelCtrl yet
        start: 0,
        range: {
          // $attrs by default gives us string values
          // nouiSlider expects numbers, so convert
          min: Number($attr.rangeMin),
          max: Number($attr.rangeMax)
        }
      });
      // When data changes inside AngularJS
      // Notify the third party directive of the change
      ngModelCtrl.$render = function() {
        $element.val(ngModelCtrl.$viewValue);
      };
      // When data changes outside of AngularJS
      $element.on('set', function(args) {
        // Also tell AngularJS that it needs to update the UI
        $scope.$apply(function() {
          // Set the data within AngularJS
          ngModelCtrl.$setViewValue($element.val());
        });
      });
    }
  };
}]);

</scirpt>

AngularJS calls the $render method whenever the model value changes inside AngularJS (for example, when it is initialized to a value in our controller).

A third-party UI component is outside the AngularJS life cycle, so we need to manually call $scope.$apply() to ensure that AngularJS updates the UI. The $scope.$apply() call takes an optional function as an argument and ensures that the AngularJS digest cycle that’s responsible for updating the UI with the latest values is triggered.

Custom Validators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<div ng-controller="MainCtrl as mainCtrl">
  <h3>Zip Code Input</h3>
  <h5>Zips are allowed in one of the following formats</h5>
  <ul>
    <li>12345</li>
    <li>12345 1234</li>
    <li>12345-1234</li>
  </ul>
  <form novalidate="">
    Enter valid zip code:
    <input type="text"
           ng-model="mainCtrl.zip"
           valid-zip>
  </form>
  </div>


<script>
// File: chapter13/directive-custom-validator/app.js
angular.module('stockMarketApp', [])
  .controller('MainCtrl', [function() {
  this.zip = '1234';
}]);
// File: chapter13/directive-custom-validator/directive.js
angular.module('stockMarketApp')
  .directive('validZip', [function() {
  var zipCodeRegex = /^\d{5}(?:[-\s]\d{4})?$/g;
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function($scope, $element, $attrs, ngModelCtrl) {
      // Handle DOM update --> Model update
      // The parser function has to return the correct value (if the data is valid) or undefined (in case the data isn’t).
      ngModelCtrl.$parsers.unshift(function(value) {
        var valid = zipCodeRegex.test(value);
        ngModelCtrl.$setValidity('validZip', valid);
        return valid ? value : undefined;
      });
      // Handle Model Update --> DOM
      // We again check for validity here and return the value.
      ngModelCtrl.$formatters.unshift(function(value) {
        ngModelCtrl.$setValidity('validZip',
          zipCodeRegex.test(value));
        return value;
      });
    }
  };
}]);
</script>

Compile

In the directive life cycle, we mentioned that a directive goes through two distinct phases: a compile step and a link step.

The compile step in a directive is the correct place to do any sort of HTML template manipulation and DOM transformation. We never use the link and compile functions together, because when we use the compile key, we have to return a linking function from within it instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<form novalidate="" name="mainForm">
  <form-element type="text"
                name="uname"
                bind-to="mainCtrl.username"
                label="Username"
                required
                ng-minlength="5">
    <validation key="required">
    Please enter a username
    </validation>
    <validation key="minlength">
    Username must be at least 5 characters
    </validation>
  </form-element>
    
  Username is 
  <form-element type="password"
                name="pwd"
                bind-to="mainCtrl.password"
                label="Password"
                required
                ng-pattern="/^[a-zA-Z0-9]+$/">
    <validation key="required">
      Please enter a password
    </validation>
    <validation key="pattern">
      Password must only be alphanumeric characters
    </validation>
  </form-element>
  Password is 
  <button>Suubmit</button>
</form>

<script>
// File: chapter13/directive-compile/app.js
angular.module('dynamicFormApp', [])
.controller('MainCtrl', [function() {
  var self = this;
  self.username = '';
  self.password = '';
}]);

// File: chapter13/directive-compile/directive.js
angular.module('dynamicFormApp')
.directive('formElement', [function() {
  return {
    restrict: 'E',
    require: '^form',
    scope: true,
    compile: function($element, $attrs) {
      var expectedInputAttrs = {
        'required': 'required',
        'ng-minlength': 'ngMinlength',
        'ng-pattern': 'ngPattern'
        // More here to be implemented
      };
      // Start extracting content from the HTML
      var validationKeys = $element.find('validation');
      var presentValidationKeys = {};
      var inputName = $attrs.name;
      angular.forEach(validationKeys, function(validationKey) {
        validationKey = angular.element(validationKey);
        presentValidationKeys[validationKey.attr('key')] =
          validationKey.text();
      });
      // Start generating final element HTML
      var elementHtml = '<div>' +
        '<label>' + $attrs.label + '</label>';
      elementHtml += '<input type="' + $attrs.type +
        '" name="' + inputName + '" ng-model="' + $attrs.bindTo + '"';
      $element.removeAttr('type');
      $element.removeAttr('name');
      for (var i in expectedInputAttrs) {
        if ($attrs[expectedInputAttrs[i]] !== undefined) {
          elementHtml += ' ' + i + '="' + $attrs[expectedInputAttrs[i]] + '"';
        }
        $element.removeAttr(i);
      }
      elementHtml += '>';
      elementHtml +=
        '<span ng-repeat="(key, text) in validators" ' +
        ' ng-show="hasError(key)"' +
        ' ng-bind="text"></span>';
      elementHtml += '</div>';
      $element.html(elementHtml);
      return function($scope, $element, $attrs, formCtrl) {
        $scope.validators = angular.copy(presentValidationKeys);
        $scope.hasError = function(key) {
          return !!formCtrl[inputName]['$error'][key];
        };
      };
    }
  };
}]);
</script>

Finally, we return a postLink function (we cannot have a link keyword along with compile; we need to return the link function from within compile instead), which adds the validators array and a hasError function to show each of the validation messages under the correct conditions.

As mentioned before, compile is only used in the rarest of cases, where you need to do major DOM transformations at runtime.

Pre- and Post- Linking

When a post-link function executes, all children directives have been compiled and linked at this point.

But in case we needed a hook to execute something before the children are linked, we can add what is called pre-link function. At this point, the children directives aren’t linked, and DOM transformations are not safe and can have weird effects.

1
2
3
4
5
6
7
8
9
{
  link: function($scope, $element, $attrs) {}
}
{
  link: {
    pre: function($scope, $element, $attrs) {},
    post: function($scope, $element, $attrs) {}
  }
}

Priority and Terminal

The last two options we look at when creating directives are priority and terminal.

priority is used to decide the order in which directives are evaluated when there are multiple directives used on the same element. For example, when we use the ngModel directive along with ngPattern or ngMinlength, we need to ensure that ngPattern or ngMinlength executes only after ngModel has had a chance to execute.

By default, any directive we create has a priority of 0.

The terminal keyword in a directive is used to ensure that no other directives are compiled or linked on an element after the current priority directives are finished.

Clean Up and Destroy

AngularJS cannot clean up event listeners we add on elements outside of the scope and HTML of the directive. When we add these listeners or watchers, it becomes our responsibility to clean up when the directive gets destroyed.

1
2
3
4
5
6
7
$scope.$on('$destroy', function() {
  // Do clean up here
});
  
$element.$on('$destroy', function() {
  // Do clean-up here
});

Watchers

These basically get triggered by AngularJS whenever the variable under watch changes, and we get access to both the new and the old value in such a case.

  • $watch, whenever the value changes, then the function passed to it as the second argument is triggered with the old and new value. Which takes:
    • A string, which is the name of a variable on the scope
    • A function, whose return value is evaluated
  • Deep $watch, The same as the standard watch, but takes a Boolean true as the third argument. This forces AngularJS to recursively check each object and key inside the object or variable and use angular.equals to check for equality for all objects.
  • $watchCollection, The function is triggered any time an item is added, removed, or moved in the array. It does not watch for changes to individual properties in an item in the array.

$apply and $digest

Whenever you’re working with third-party components, remember that there are two distinct life cycles at play. The first is the AngularJS life cycle that is responsible for the keeping the UI updated and the second is a third-party component’s life cycle.

And this is done by triggering $scope.$apply(), which starts a digest cycle on the $rootScope.

Sometimes, another event in AngularJS will automatically trigger and take care of this, but in any case if you are updating any scope variables in response to an external event, make sure you manually trigger the $scope.$apply() or $scope.$digest().

文章目录
  1. 1. What Are Directives
  2. 2. ng-include
    1. 2.1. Limitations of ng-include
  3. 3. ng-switch
  4. 4. Creating a Directive
    1. 4.1. Restrict
    2. 4.2. The link Function
    3. 4.3. Scope
    4. 4.4. Replace
  5. 5. Unit Testing Dirctives
  6. 6. AngularJS Life Cycle
    1. 6.1. The Digest Cycle
    2. 6.2. Directive Life Cycle
  7. 7. Transclusions
    1. 7.1. Advanced Transclusion
  8. 8. Directive Controllers and require
  9. 9. require Options
  10. 10. Input Directives with ng-model
  11. 11. Custom Validators
  12. 12. Compile
    1. 12.1. Pre- and Post- Linking
  13. 13. Priority and Terminal
  14. 14. Clean Up and Destroy
  15. 15. Watchers
  16. 16. $apply and $digest