angularjs-Directives
更新日期:
- 1. What Are Directives
- 2. ng-include
- 3. ng-switch
- 4. Creating a Directive
- 5. Unit Testing Dirctives
- 6. AngularJS Life Cycle
- 7. Transclusions
- 8. Directive Controllers and require
- 9. require Options
- 10. Input Directives with ng-model
- 11. Custom Validators
- 12. Compile
- 13. Priority and Terminal
- 14. Clean Up and Destroy
- 15. Watchers
- 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
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 }); }; } }; }]); |
- Get the
$compile
service injected into our test. - Set up our directive instance HTML.
- Create and set up our scope with the necessary variables.
- Determine the template to load because our server is mocked out.
- Instantiate an instance of our directive using the
$compile
service. - 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:
- 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.
- The HTML page finishes loading.
- When the document ready event is fired, AngularJS bootstraps and searches for any and all instances of the ng-app attribute in the HTML:
- 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.
- AngularJS takes the link function and combines it with scope.
- 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:
- 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
- AngularJS also keeps track of all the elements that are bound to the HTML for each scope.
- When one of the events mentioned in the previous section happens, AngularJS triggers the digest cycle.
- 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.
- If nothing has changed, it recurses to all the parent scopes and so on until all the scopes are verified.
- If AngularJS finds a watcher at any scope that reports a change in state, AngularJS stops right there, and reruns the digest cycle.
- 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.
- AngularJS reruns the digest cycle every time it encounters a change until the digest cycle stabilizes.
- When the digest stabilizes, AngularJS accumulates all the UI updates and triggers them at once.
Directive Life Cycle
- When the application loads, the directive definition object is triggered. This hap‐ pens only once.
- Next, when the directive is encountered in the HTML the very first time, the tem‐ plate for the directive is loaded.
- 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.
- 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
- 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.
- First, we tell the directive that we are going to use transclusion as part of this di‐ rective.
- 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()
.