Chapter 3 - AngularJS Component Primer Part One: The View
The AngularJS.org landing page succinctly summarizes the framework as “HTML enhanced for web apps!” A directive is the AngularJS term for a unit of this extended HTML. Directives may encapsulate other directives and more than one directive may be active on an HTML element at a time. AngularJS has many built-in directives that form the nucleus of the declarative aspect of the framework.
The core directives cover the most common dynamic functionality for the view, but as we get beyond the core where we need to define our own dynamic view functionality, we do so by creating our own directives with a directive definition object.
AngularJS Core Directives
The first place to learn about AngularJS’ core directives is the AngularJS documentation. Our discussion aims to expand on the documentation with a bent towards componentization. The API docs should be consulted for full API usage of any core directives discussed.
http://docs.angularjs.org/api/ng`
However, there are several core directives that are of particular importance in understanding how the framework may be used to transform an HTML document from static to dynamic, and the core directives also fall into a few different categories. It is important to understand these as well. Our goal is to provide some insight to the core directives that is complimentary to the official documentation.
Scoping Directives ng-app, ng-controller
ng-app
An AngularJS application within an HTML document is scoped or demarcated with ng-app by placing this directive in the element that will serve as the root of the application. Most often this would be the root of the document, either <html> or <body> if we are creating a single page app. But it can be located further down the DOM hierarchy if in situations where we may want to run an AngularJS app within a single page app of another framework.
Example usage would be:
<anyElemement ng-app="myAppModule">
The dependencies for ng-app would be the core angular.js and a user defined AngularJS module that will serve as the application module.
Also, a remainder that for exact syntax and a simple usage example of any core directive, please refer to the API documentation at http://docs.angularjs.org/api/.
ng-controller
AngularJS will automatically instantiate a default $rootScope for any declared application. But this is hardly useful to us. To give an application life, we need to define controller(s) for our application’s business logic, and we need to “declare” the DOM scope of our controllers by placing the ng-controller directive in an element within our application boundary:
<anySubElement ng-controller="myAppController">
For both of the above to work, and to avoid the dreaded “myAppModule does not exist” error we need code in our HTML document that accomplishes the following:
3.0 Basic AngularJS app outline
<!doctype html>
<html ng-app="myAppModule">
<head>
<script src="angular.min.js"></script>
<script>
angular.module('myAppModule', [dep_1, dep_2, dep_3])
.controller('myAppController', ['$scope', function($scope) {
$scope.someData = 'Hello World!';
}]);
</script>
</head>
<body>
<div ng-controller="myAppController">
...
</div>
</body>
</html>
The most common source of JavaScript console errors (or lack thereof) for AngularJS newbies is caused by forgetting to include either the declaration or matching definition for a module or controller.
Every controller has an associated $scope object which as you may recall is the VM of the MVVM. Controller directives may be nested within other controller directives which is how we would go about creating a $scope hierarchy akin to the DOM hierarchy. We will discuss $scope in depth in chapter 4.
ng-app has one major drawback in that there can only be one of these per HTML document. So what happens if we have more than one AngularJS application that needs to reside in the same HTML document? Such may be the case if we need to include more than one unrelated AngularJS UI component of which each would technically be considered an “app” from the AngularJS point of view. Fortunately we can instantiate multiple AngularJS apps imperatively using:
angular.bootstrap(element[, modules]);
where element is a jQuery or jQlite reference to the element in the DOM. This code should only be called when all the necessary dependencies have loaded such as on domReady or load events.
Event Listener Directives
Remember the days back in the 90’s, before jQuery when adding JavaScript to a page consisted primarily of something like this:
<a onclick="alert('hello world')" href="http://angularjs.org/">
If you don’t, then you are lucky. We had to stop doing that because around the time AJAX became the buzzword-of-the-day, it came to be considered “obtrusive JavaScript” since we were “mixing” presentation with behavior. So we had to maintain our JavaScript in separate files and reference an associated DOM element like so:
3.1 Unobtrusive JavaScript - the jQuery way
<!-- presentation.html -->
<a id="some_anchor" href="http://angularjs.org/">
/* behavior.js */
$('#some_anchor').click(function(){
alert('hello world');
});
Now we have presentation and behavior cleanly separated, but as the presentation and behavior grew to thousands of lines of code, managing the points of association or bindings became very messy and loaded with boilerplate code. Even worse, it lead to JavaScript that was very difficult to unit test since the DOM reference became a hard external dependency.
The AngularJS team gave the two issues above some thought, and their solution was to try and find some middle ground while incorporating the advantages of both approaches. The result was to essentially to move the event listeners back into the DOM while still keeping the business logic separate. This effectively removes the binding boilerplate and management headaches.
So the core AngularJS module offers a number of directives to match, more or less, the old DOM attribute listeners. Currently, in version 1.2, the list includes:
ngBlur, ngChange, ngClick, ngCopy, ngCut, ngDblclick, ngFocus, ngKeydown, ngKeypress, ngKeyup, ngMousedown, ngMouseenter, ngMouseleave, ngMousemove, ngMouseover, ngMouseup, ngPaste, ngSubmit
Very similar to the old listener attributes, an AngularJS listener directive can be included as an element attribute and execute an AngularJS expression on the event.
3.2 The AngularJS way
<!-- view.html -->
<a ng-click="popup(message)" href="http://angularjs.org/">
/* controller.js */
function demoCtrl($scope, $window){
$scope.message = "hello world";
$scope.popup = function(msg){
$window.alert(msg);
};
}
“popup()” and “message” would map to a function and a variable defined on the current scope respectively. More on this in the next chapter. But suffice to say, we are NOT executing a function on the global scope as we would be with JavaScript event listeners.
Also please note that attribute directive names in the form of ng-* are automatically camel-cased when mapped to the directive definition in our module JavaScript. Directives in the form of HTML attributes are the preferred way of inclusion, but there are other ways (comments, elements, data-*) of including directives in markup explained in the AngularJS documentation.
DOM & Style Manipulation Directives
Most of the remainder of the core directives are utilized to dynamically modify the DOM in one way or another.
ngShow and ngHide are the jQuery equivalents to .show() and .hide().
ngClass, ngClassEven, ngClassOdd, ngStyle are used to manipulate styling based on the state of the associated $scope.
ngIf, ngRepeat, ngSwitch are used to add some conditional logic for template inclusion/exclusion in the DOM. It’s a best practice to avoid logic in templates where possible. However, in cases where it makes the source code significantly DRYer, or it is not desirable to have asset URLs evaluate, then its warranted.
ngInclude, ngBindHtml, ngBindTemplate are used to include template HTML into the document.
Form Element Directives
AngularJS provides a very rich set of directives for manipulating forms, form elements, and form data. Several of the directives are mapped directly to common form elements themselves to augment with AngularJS behavior like form, input, select, textarea.
ngForm allows for nested forms.
ngModel binds the value of any form element to a property on the scope.
AngularJS’ form directives enable extensive declarative functionality including validations, filtering, and persistence. Two or three chapters could be devoted to form directives alone, but unfortunately are not in the $scope (pun intended) of this book.
Miscellaneous Directives
There are a number of core directives that have special purposes.
Some have been created to prevent the momentary flash of unstyled AngularJS text (FOUT) that can happen in the milliseconds between DOM paints and AngularJS evaluation including ngHref, ngSrc, ngCloak and ngBind. ngBind is used in place of the standard AngularJS expression delimiters {{ }}.
ngInit is used when it is desired to declaratively initialize scope variables. Care should be used in applying ngInit as it can be easy to allow controller code to bleed into the view.
ngNonBindable is used to prevent AngularJS from evaluating any expressions within the containing element. It can be thought of as applying commenting delimiters like <!-- --> would be used to prevent HTML evaluation and rendering. This comes in handy for things like code snippets in HTML text, or if you find yourself in certain, odd situations where there may be conflicts between AngularJS delimiters {{ }} in templates that also may serve as a Handlebars.js or html.twig templates since they use the same characters.
ngTransclude is a special directive that allows merging the content of a parent element content into a child directive. This one has particular relevance to building custom directives and will be discussed in depth in subsequent sections.
The list of AngularJS core directives is constantly growing and evolving, so please refer to the official documentation for the latest.
Rolling Our Own Directives
Understanding the dynamic behavior that can be achieved through inclusion of AngularJS’ core directives gives us a good basis for understanding how we can encapsulate our own custom behavior wrapped as a directive.
There are a number of important directive building blocks and life-cycle states that need to be fully understood before we can master the art of rolling our own directives. We will take a look at each of these from the perspective of our goal to be able to use AngularJS’ directives as a tool for creating custom widgets or UI components. One of the requirements of component architecture is that the component should be able to exist independently of its containing environment and not need to know anything about it. We must draw this distinction because AngularJS directives have many other uses in providing custom behavior for web apps which do not require them to be entirely self-contained, and not understanding the distinction is where noobs often let outside dependencies bleed in.
For a full, general understanding of directive creation, which is beyond the scope of the book, I recommend first the section in the AngularJS docs on directives, and also chapters 8-9 in Mastering Web Application Development with AngularJS by Darwin and Kozlowski.
http://docs.angularjs.org/guide/directive
http://docs.angularjs.org/api/ng.$compile
http://www.packtpub.com/angularjs-web-application-development/book
Directive Declaration
For our purposes, a minimal UI component directive definition that replaces a custom HTML tag in a page might be something like:
3.3 Minimal component directive
var myAppModule = angular.module('myApp', []);
myAppModule.directive('myHello', function() {
return {
template: '<div>Hello world!</div>',
restrict: 'E',
replace: true
};
});
Including in pre-parsed, pre-compiled markup:
<html lang='en' ng-app='myApp'>
...
<body>
<my-hello></ my-hello>
</body>
...
Viewing in AngularJS parsed and compiled markup:
<body>
<div>Hello world!</div>
</body>
Directive Naming
Our directive name is myHello. The my part is to provide a namespace for our widget should we want to include and distinguish it as part of widget library separate from others- especially the AngularJS directive library (ng). Name-spacing custom directive is considered a best practice in AngularJS development.
When the name myHello is registered with AngularJS, AngularJS will try to match that name when it parses the DOM looking for “my-hello” and performing an auto camel case between markup and code. By default, AngularJS searches for an HTML attribute such as <div my-hello>, but in our case, the line of code above, restrict:’E’, tells the AngularJS parser to search only for custom named elements <my-hello> rather than element attributes.
AngularJS actually gives four options for directive declaration as: elements, attributes, classes, and HTML comments denoted by E, A, C, and M respectively. The options provide for situations when supporting legacy browsers such as Internet Explorer 8 or where the HTML must be validated. Neither of these situations are particularly relevant for current HTML5 compliant browsers, and we want to be as forward compatible as possible with W3C Web Component specifications, so we will only be declaring our directives as HTML elements throughout this book.
|
The W3C spec for custom elements requires that custom element names include a hyphen in them. It’s good practice to include a hyphen in the names of any element directives defined with AngularJS 1.x for Web Component forward compatible code. The prefix before the hyphen should be your library’s three letter namespace. |
Directives Definition
The restrict:’E’ line above is part of a return statement which returns what’s called a directive definition object from our directive registration function. The definition object is where we configure our directives to do all sorts of things including overriding certain configuration defaults.
As of AngularJS 1.2, the current directive configuration options include: priority, restrict, template, templateUrl, replace, transclude, scope, controller, require, link and compile. In order to be thoroughly fluent in rolling our own directives, we need to be intimately familiar with all of these configuration parameters including their options, defaults, and life-cycle order if pertinent. Again, for a comprehensive reference, please see the documentation links above since the discussion here is meant to expand upon that knowledge for our purposes.
3.4 Options and Defaults for the Directive Definition Object
myModule.directive('myHello', function factory(injectables) {
var directiveDefinitionObj = {
priority: 0,
// HTML as string or a function that returns one
template: '<div></div>',
// or URL of a DOM fragment
templateUrl: 'directive.html',
replace: false,
transclude: false,
// directive as attribute name only
restrict: 'A',
scope: false,
controller: function($scope, $element, $attrs, $transclude,
otherDependencies) { ... },
// string name of directive on same element
require: 'directiveName',
compile: function compile(tElement, tAttrs, transclude*) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
// return function postLink( ... ) { ... }
},
// or
link: {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
},
// or
link: function postLink(scope, iElem, iAttrs, ctrl, transcludeFn)
{ ... }
};
return directiveDefinitionObj;
});
template, templateUrl, and replace
In our example above we also included configuration values for template and replace. template tells our directive to use the string or function return value as an inline HTML template for our directive, and replace tells our directive to replace the parent element with the template rather than appending it. The template configuration is mutually exclusive with the templateUrl configuration. The latter is used to load an external template file, or include a previously loaded template as <script> tag. There is some debate as far as when to use which template inclusion style that is very pertinent to our goal of encapsulated UI components.
The motivation for loading an external DOM fragment includes:
- ease of maintenance and editing of HTML source code longer than 1-2 lines
- less cluttering of directive definition source code
- easier replacement
The motivations for including inline templates include:
- self-contained, encapsulated widget code analogous to how we might package Web Component code
- fewer files to manage
- better load performance than a separate external file
- being the only option for certain browser security restrictions such as loading cross-domain or cross protocol
For our purposes, the ideal situation would be a combination of the above. For development we maintain a separate template file. For production, we stringify the template source and inline it into the directive source via a build script prior to minification. We will explore a potential build script for this process in Chapter 7.
AngularJS Templates are Real HTML, not Strings
A quick tangent on AngularJS templates is relevant. AngularJS templates, unlike other front-end template systems like Handlebars.js, are composed of real HTML fragments rather than text strings that undergo a compile and token replacement step. This is because AngularJS operates directly on the static HTML in a page, and the HTML in the page is the AngularJS template. For our purposes, what this means is that our template HTML must be an actual hierarchy of nodes including all opening and closing tags with a root node. Failures to include a closing tag, mixing tags, or including things like HTML comments or <style> tags before or after the root node in templates are very common cause of fatal errors for AngularJS as of version 1.2. Hopefully in future releases the AngularJS compiler will be more permissive, as it is a drawback for building fully self contained JavaScript, HTML, and CSS not to be able to include a <style> tag prior to and at the same level as the root node in the template.
3.5 Valid AngularJS HTML Template
<div class="single-root-element">
<!-- a text node -->
<h1>my template</h1>
<div> {{ myContent }} </div>
</div>
3.6 Invalid AngularJS HTML Template
<!-- a text node -->
<h1>my template</h1>
<div> {{ myContent }} </div>
|
08/18/14 update - In AngularJS 1.2+ both templates above are now valid as the templating engine is more permissive towards the structure of the HTML fragment. |
The final directive definition configuration in our previous example is replace. AngularJS’ default is false (boolean value) meaning that any contents of our directive will be appended to the parent element rather than replacing it. This behavior is often desirable for directives that are part of AngularJS applications, however for packaged UI components and widgets, it is much cleaner if the parent element functions as a declaration and set of parameters to be entirely replaced by the processed directive. The only exception might be when we want to set up a live binding between a parent attribute and a directive scope value. However, this can usually be achieved in a cleaner fashion using event $broadcast and $emit to be discussed in the next chapter.
3.7 When replace equals true vs. false
<div class="myDirectiveTemplate">
<span> some inner content </span>
</div>
...
<my-directive>
<span> some outer content </span>
</my-directive>
<!-- when replace = true becomes: -->
<div class="myDirectiveTemplate">
<span> some inner content </span>
</div>
...
<!-- when replace = false becomes: -->
<my-directive>
<span> some outer content </span>
<div class="myDirectiveTemplate">
<span> some inner content </span>
</div>
</my-directive>
|
08/18/14 update - For the purposes of Web Components and custom element forward compatibility, it probably is best now to stick with |
require, priority, and terminal
require: 'directiveName' or require: [directive names]
is used to require that other directive(s) be present on the same element as ours. Prefix the “^” to the directive name to require the directive be present on any ancestor element. AngularJS apps may make extensive use of multiple directives on elements to ensure that any presentation or behavioral dependencies are present. When using AngularJS directives as a tool to create discreet UI components or widgets, especially those of a style congruent with Web Components, we really want the top level directive that represents our custom HTML non-dependent on other modules. That said, we are free to style any inner directives as we see fit since we may have the complexity of a miniature application nested within our component. By Requiring another directive, our directive is requesting methods and properties from the required directive’s controller instance be available to its controller instance. This is analogous to the “mixin” way of reusing functionality in JavaScript, and a way that we can keep our source code DRY.
Required directives can be made optional if we prepend a “?” (“?^” for searching parents) to the directive name, require: ‘?directiveName’. Otherwise AngularJS will throw an error if the required directive is missing from the element.
When we require a directiveName its controller instance becomes available as the fourth parameter on our link function. If we need to require multiple directives, then the fourth parameter would an array of controller instances:
3.8 require Options for the Directive Definition Object
myModule.directive('myHello', function factory(injectables) {
var directiveDefinitionObj = {
// directive controller must be found on current element; throws error
require: 'directiveName',
// directive controller optional on current element; no error if missing
require: '?directiveName',
// directive controller must exist on any ancestor element; throws error
require: '^directiveName',
// directive controller optional on any ancestor element; no error
require: '?^directiveName',
// the controllers associated with the required directive(s) are injected
link: function postLink(scope, iElem, iAttrs, controller/[controllers]) {
...
}
};
return directiveDefinitionObj;
});
For UI component level directive definitions we should think twice about using the require option as it implies either hard or soft dependencies on the outside DOM. Dependent functionality should be injected as a service, and necessary configuration or data are better injected via attributes. At the time of this writing, if we want to be sure our UI components are forward compatible with the W3C Web Components specification, then we don’t want to have multiple directives residing on the same component element since that is not supported as a part of the custom element spec.
priority is a number used to specify execution order of multiple directives on an element. Higher numbers are applied earlier. The default is 0. As with require, we probably want to omit this for our encapsulating directive while being free to use it for any inner directives. terminal (boolean) specifies that the current priority is the last to execute on an element.
The remaining five fields of the definition object deserve more extensive discussion and need their own sections.
scope
The scope option is where we declare what, if any, parent scope data will be accessible to our directive component. It is also where we may declare any defaults for the data bound to our view. The scope will become for all intents and purposes, the ViewModel if we see our component as an MVVM implementation. It’s the glue for binding our data together with our view.
Discussion of scope, data, and models are quite important so we will defer to and devote the entire next chapter to this topic. For now, just know that scope: false means use whatever scope object instance is the element level of our directive. scope: true says to create a new scope object instance for our directive but inherit the properties and methods of the parent scope to our directive element. scope: {…} says to create a new isolate scope object instance that does not inherit from any parent scopes, but still allows us to selectively import properties and methods from parent scopes. At the component level, we will almost always want to create an isolate scope since our components should not know about the world outside their boundaries,
transclude
transclude is a word made up by the AngularJS team. Transclusion allows us to compile the DOM contents of the directive element and insert into the directive template on a node with the core directive ngTransclude. By default the contents are transferred as is, but AngularJS also gives us the option to perform some intermediate manipulation. The interesting thing about as is, is that this also includes the scope of the original DOM fragment along with the markup. The analogy is much like that of a closure in JavaScript where the return value retains access to scope values in the originating function.
The default value of transclude is false. If set to true, we can transfer the DOM fragment, original scope and all, into any <element ng-transclude> node in our directive template. The transcluded DOM and other nodes in the template may contain the same data-binding identifiers such as {{name}}, but they can hold different values as set on their respective scopes.
We may also set the value to ‘element’, in which case the entire directive element including any other directives on it, may be transcluded.
Since this book is focused on UI components the desire to inject DOM fragments may seem a bit puzzling. However, consider that there is no requirement that a UI component supply all of it’s own HTML in its template. Many UI components are simply just containers such as tab panels, headers, or footers. Suppose in a large organization with many micro-sites our goal is to propagate the official corporate look and feel. We can create a DOM container component with the look and feel, and allow the site developer to import their contents, scope and all, via transclusion. If there is a need to perform any manipulation on the transcluded DOM, AngularJS passes a $transclude function as the fifth parameter to a directive’s linking function. This can be useful for validating the passed in content, but care should be taken that we are not setting up a situation where our component is forced to make assumptions about what’s passed in a way that it needs to know about the outside world.
Transclusion is one of the more advanced AngularJS concepts to grasp, so it is best explained by an example of a useful scenario below. However, suffice to say that transclusion can be a very powerful way to inject DOM fragment dependencies into our UI components.
3.9 transclude Example
// simplified for this example, never hard code URLs in your controller code
myApp.controller('myFooter', function($scope){
$scope.faceBookUrl = 'https://www.facebook.com/dgs';
$scope.googlePlusUrl = 'https://plus.google.com/10081026737705520/videos';
$scope.twitterUrl = 'https://twitter.com/dgs';
$scope.faceBookImg = 'https://cdn.dgs.com/img/fb.jpg';
$scope.googlePlusImg = 'https://cdn.dgs.com/img/gp.jpg';
$scope.twitterImg = 'https://cdn.dgs.com/img/twitter.jpg';
});
<!-- before compilation and transclusion-->
<footer ng-controller="myFooter">
<!-- we have provided a social icon container component the page
author can fill with links and images -->
<social-links-container>
<a href="{{ faceBookUrl }}"> <img src="{{ faceBookImg }}"> </a>
<a href="{{ googlePlusUrl }}"> <img src="{{ googlePlusImg }}>" </a>
<a href="{{ twitterUrl }}"> <img src="{{ twitterImg }}>"> </a>
</social-links-container>
</footer>
myApp.directive('socialLinksContainer', function factory(){
return {
template: '<div ng-transclude id="social-container"'+
'class="corporate-styling"></div>',
transclude: true,
// replace the directive element
replace: true,
// isolate scope
scope: {},
controller: function($scope, $element, $attrs, $transclude){
// this won't be applied to the transcluded template
$scope.facebookUrl = 'http://facebook.com/dgs';
// $transclude here is identical to transcludeFn below
},
link: function(scope, elem, attrs, ctlrs, transcludeFn){
transcludeFn([scope], function(clone){
//clone is the transcluded contents we can manipulate
//perform any processing on transcluded DOM
//the optional scope parameter would be a replacement
//for the default
//transclusion scope which is the parent of the directive scope
});
}
};
});
<!-- after compilation and transclusion -->
<footer ng-controller="myFooter">
<div id="social-container" class="corporate-styling">
<!-- notice from which scope the href value is applied -->
<a href="https://www.facebook.com/dgs">
<img src="https://cdn.dgs.com/img/fb.jpg">
</a>
<a href="https://plus.google.com/100810267337987705520/videos">
<img src="https://cdn.dgs.com/img/gp.jpg>"
</a>
<a href="https://twitter.com/dgs">
<img src="https://cdn.dgs.com/img/twitter.jpg">
</a>
</div>
</footer>
controller
The controller option of our directive definition object is a constructor function that gets instantiated before any pre-linking functions are executed. This is the place to attach any methods and properties to be shared among all instantiated directives of the type we are defining. In MVC terms, it would be the “C”.
3.10 Controller Option of a Directive Definition Object
controller: function('myHelloCtrl', ['$scope', '$element',
'$attrs', '$transclude', 'otherDeps',
function($scope, $element, $attrs, $transclude, otherDeps){
...
}]),
Common injectables include the instance scope, element, attributes, and sometimes transcluded DOM. As with the other options for declaring controllers, the declaration syntax in our directive supports the “long hand” version which allows it to survive minification.
Naturally our controller function is where we would want to define any common business logic for all instances of our directive definition object. This is very important to distinguish from the directive’s linking function where any instance specific properties and methods should be defined especially where we may have many instantiated component directives in the same document. This is also where we would define any API methods and properties that may be required in by other directives. A very common mistake for developers unfamiliar with the directive lifecycle is to declare methods and properties here that are really intended to be instance specific.
compile and link
These options are a rich source of confusion among AngularJS noobs. Understanding their differences and relationship really requires one to understand the basic architecture underlying AngularJS and how it performs its data-binding magic. Part of the confusion can be traced to all the different ways that compile and link syntax is displayed in examples of AngularJS code. Understanding the purpose and relationship of each, as well as, the directive lifecycle (explained in a subsequent section) is necessary to know which syntax to use.
You would specify either one or the other option in your directive definition. As with compiled software languages, naturally compiling happens before linking. In the AngularJS world, the compilation step during the birth of a directive instance is where any necessary manipulation of the template DOM prior to use would take place. Any manipulations or transformations would be common to all directive instances. While the need to manipulate a template before marrying with a scope and inserting in the DOM is not common, there are situation where we may need to use this step. Since AngularJS uses real HTML for its templates, any situation where the template may have strings that need to be transformed into HTML elements first would be a use-case, as would any DOM cloning or conditional inclusion.
The compile function will always return a function or an object that will be used for the linking step where the template is cloned and married with its designated scope including any instance methods or properties. When returning a function, the function will execute during the post-link step. When returning an object it would include functions to execute during the pre-link step and the post-link step.
Which syntax is chosen should depend on whether or not there is a need to perform a task before any child elements or directives are processed. The pre-link function on a directive-element executes before the pre-link function of a child directive-element and so on until the bottom of the directive hierarchy is reached. Conversely, the post-link function of the lowest directive in any hierarchy will execute before the post-link function of any directive-element above it. For those familiar with the history of the event propagation model where event-capturing down the DOM hierarchy happens before event bubbling back to the root of the document the analogy here is quite similar. Just as we almost always process browser events during the bubbling phase, so do we usually perform any directive instance processing during the post-link phase. You can be sure that you are only affecting the instantiated directive-element when adding or manipulating any methods, properties or injectables in the post-link function.
3.11 Compile and Link Syntax Options for Directive Definitions
// use when logic is needed in the compile, pre, and post link phases
compile: function compile(tElement, tAttrs) {
... //compiler logic
return {
pre: function preLink(scope, iElement, iAttrs, ctlr, transcludeFn){...},
post: function postLink(scope, iElement, iAttrs, ctlr, transcludeFn){...}
}
},
// use when logic is needed in only the compile and post link steps
compile: function compile(tElement, tAttrs) {
... //compiler logic
return function postLink(scope, iElement, iAttrs, ctlr, transcludeFn){...}
},
// use when logic is needed only during the pre and post link phases
link: {
pre: function preLink(scope, iElement, iAttrs, ctlr, transcludeFn) {...},
post: function postLink(scope, iElement, iAttrs, ctlr, transcludeFn) {...}
},
// use when we only need to perform logic in the post link step - most common
link: function postLink(scope, iElement, iAttrs, ctlr, transcludeFn) {...}
A side note on avoiding accidental memory leaks is to always make sure any event listeners within our directive-element are attached only in the post-link function, and always utilize the $destroy event to cleanup any bindings on a directive-element to be removed.
You may notice in the AngularJS documentation that the injectable parameters for compile functions include tElement and tAttrs, and the injectable parameters for the link functions include iElement and iAttrs. The “t” denotes template, and the “i” denotes instance to help keep the difference of what’s actually being injected clear. Actually all the parameters to a link function are either instances or functions that operate on an instance. The *Attrs parameters in both cases provide a normalized list of camel-cased attribute names to the function. Also, it is the order of the parameter injectables that is important, not the name.
Dependency Injection (API) Strategies
Now that we have an overview of the AngularJS API for configuring directives let’s discuss how we can leverage these configuration options to create our own API’s for our directives that would act as reusable and portable UI components.
Static Attributes
The first thing to consider is who the consumer of your component is going to be. Will they be a designer who has little if any knowledge of JavaScript and AngularJS, or will they be a senior front-end developer? If they are the former, then we likely want to create any configuration APIs as declarative and plain English as possible. This would essentially restrict us to utilizing static element attributes as the way to pass in any configuration information. A UI component directive would look something like:
<mySearchBox size="small" auto-complete="true"></mySearchBox>
In our directive definition we could access the attribute values in either the scope or link options:
scope: { size: @, autoComplete: @} //or
link: function(scope, elem, attrs){
scope.size = attrs.size || 'small';
scope.autoComplete = (attrs.autoComplete === 'true') ? true : false;
}
Pulling in attribute values in the link function allows for a bit more flexibility to set defaults or perform any processing.
Dynamic Attributes, Functions & DOM Fragments
If our component customers are more JavaScript savvy we have more options in terms of how the interaction with our components can be set up. In addition to static attribute values, we could also permit scoped functions, AngularJS DOM fragments, and dynamic attribute values to be passed into our components. Along with the additional flexibility, we also need to be wary that we are not setting up any undesirable tight dependency couplings at run-time. Since we can inject live, two-way bindings including full DOM fragments, a best practice would be to make sure that our component can either function without the dependency or that a default is provided internally. A worst practice would be to rely on try-catch blocks for robustness, one that is epidemic in enterprise JavaScript.
Full DOM fragments can be included as innerHTML in our custom component element via transclusion:
<component>
<inner-html></inner-html>
</component>
transclude: true,
<template-html ng-transclude>...</template-html>
Any HTML passed in, includes the original scope context. Alternatives for injecting any scope context as an API parameter include the “=” and “&” prefixes in the scope definition for two-way binding and scoped functions respectively. These are discussed further in the next chapter when we talk about scope.
We could also inject full AngularJS controllers via the require option, but then we have to ask whether or not we’ve designed our directive to be a discreet component to begin with.
If we happen to be including our AngularJS UI component inside of another application level framework such as Backbone.js with Handlebars.js for templating we can set directive attribute values dynamically at runtime as template tokens and then lazily instantiate our AngularJS UI component. This is a very advanced topic, but also the key to slowly transforming bloated jQuery based web applications to concise AngularJS apps from the inside out, with out the need to perform a full rewrite from scratch!
// this could be a Handlebars.js template
<mySearchBox size="{{small}}" auto-complete="{{auto}}"></mySearchBox>
What was mentioned in the last couple paragraphs will get extensive treatment by example in the next part of this title since these are core concepts to understand for building robust UI components and solving real world problems in existing web applications such as code bloat and excessive boilerplate.
|
08/18/2014 update - For AngularJS UI component source code that is web component ready, it’s now best to try to limit input APIs to attributes and events, and to limit output APIs to events where possible. UI components that are developed with these restrictions will be much more cross-compatible with other web components when the UI components created with AngularJS are, themselves, upgraded to web components. |
The Directive Lifecycle
Understanding the steps in the lifecycle of a directive is important for knowing what can be manipulated when. We particularly want to avoid performing operations intended for a single directive instance on all instances by mistake.
Consider the run-time of a browser page, and assume all AngularJS bootstrap prerequisites have been loaded and included. When the initial DOM has been flowed and painted, the onDomReady event is fired and AngularJS begins the bootstrap process. After all modules are registered, AngularJS traverses the DOM under it’s control and matches any names to registered directives. When a name is matched, any associated HTML template is compiled into a template function. Keep in mind that the very first DOM traversal is actually a template compilation since AngularJS uses real HTML nodes (not strings) as templates.
If there are multiple directives attached to the same DOM node, the compiler sorts them by priority. Then the compile functions of matched directives are called making any compile step potentially a recursive operation. During the compilation step is where any necessary DOM manipulation takes place. As every compile function is run a linking function is returned which includes injectables for scope, element, attributes, any additional required controllers plus any developer added functionality.
Note that directive controller functions are constructors that are instantiated prior to any linking function execution. Therefore, any properties or methods in the constructor are copied to the instance and any prototype properties or methods are shared. As with basic JavaScript inheritance, any initial controller or scope properties should be included in the constructor. This is a good place to perform any general initialization. Methods including common functionality to all directives of the same type should be on the prototype object to avoid needles copies.
Linking functions are by default executed from the bottom of any controller hierarchy up, unless they are designated specifically as pre-link functions. Pre-link functions are executed top down and where any preprocessing should take place. When the link function is executed, the compiled template is bound with the associated scope instance where $watches and listeners are set up. The linking function is where we have access to the live scope and would perform any final initialization. Any AngularJS expressions in the linking function are equivalent to any included in an ng-init directive.
At this point, any AngularJS interactive app or component directives are ready for human interaction, and remain so until destroyed by a page refresh or client-side operation. AngularJS operations that destroy templates are usually pretty good about removing associated binding, but not always. If heavy use of a custom directive results in a memory leak, then $destroy() (discussed later) needs to be applied manually.
This above description doesn’t suffice as a complete, illustrative or definitive explanation of the directive life-cycle process. I recommend reading about directive compilation from multiple sources including the docs and general AngularJS books out there. The purpose here is to create awareness of what developer tasks should and can be performed at what part of the directive creation process.
Testing Directives
If your goal in reading this book is to create AngularJS UI components that other developers will want to use, then comprehensive testing is a must, both unit and end-to-end. There are many approaches and frameworks for creating test cases. Jasmine for behavioral unit testing, Karma as a test runner, and Protractor for end-to-end are the preferred frameworks in the AngularJS community. Karma and Protractor were actually created by the AngularJS team.
There are also many good examples of creating unit tests in the tutorials, the best being those for the AngularJS ng-core directives at:
https://github.com/angular/angular.js/tree/master/test/ng/directive/
These should be used as the definitive set of unit testing examples.
We are not going to get into code examples for testing until the comprehensive examples in later sections. For now, the point should be made that if you create a pallet or library of UI components using AngularJS directives, the APIs for your components will be the contract between you and your customer. Your unit tests will be the means of verifying that contract during the lifetime of your library as you upgrade the internal source code.
For those unfamiliar with research methodology there are two important concepts with regard to the results that your tests provide:
- reliability
- validity
Validity refers to the degree that your test measures what it is supposed to measure. Reliability is the degree to which your test consistently measures from one run to the next. A lot of code shops require developers to provide unit tests along side their source code, and a lot of these tests are crap because they do not measure the primary piece of logic in the function. They measure something inconsequential and are written to always pass. These shops are often the source of excessively bloated and poor quality code to begin with.
If you do bother to provide tests with your source code, then make sure your tests are measuring the primary piece of logic in your function. It should paraphrase in code the documentation for the function.
Also, in the ideal world, we as developers have set requirements that we can create our TDD code against. The reality in many enterprise organizations is that a byproduct of a dysfunctional organization is developers being forced to code against fuzzy, changing requirements. If your product managers cannot deliver set specifications before your engineering manager starts cracking the coding whip, then test driven development is pointless since the logic to be tested will likely change right up until the QA process or even production. My two cents in this situation is to wait until the requirements are reasonably stable before writing tests, and consider any source code produced up until that time to be only of prototype quality.
Summary
In this chapter, we discussed AngularJS directives, both of the shelf and custom. We also touched on specific ways that AngularJS directives can be defined to function as robust UI components of a quality that can be reused, exported, imported, published, etc.
In a web architecture sense, AngularJS directives encapsulate and represent the aspects of our application that relate to the View in an MVC, MVVM, or MVwhatever framework since AngularJS really has both controllers and view-models. Our view code, along with all of our code, should be decoupled of global and application level dependencies. If we are creating directives to serve as UI component encapsulation, then they should be created in such a way that no knowledge of the outside page or application is required. Likewise, the should be set up so that the containing page, app, or container can inject all the necessary configuration and data.
In the next chapter we discuss UI component architecture from the Model and Controller perspectives.