Angular is awesome. Directives, on the other hand, can be a pain in the ass. They seem complex – their place in the application sometimes seems unclear, and using them properly is practically an art form. Fortunately anyone can learn why directives are the beating heart of the Angular framework, and with some instruction and practice begin writing their own. Today!
So let’s get started.
How Directives Wake Up
Angular applications have two distinct phases: Compilation and Runtime. Compilation is a multistep process that begins when the browser fires the DOMContentLoaded event. When this event fires – and assuming the ng-app directive was attached to an element – Angular will automatically start loading. If the ng-app directive is missing Angular will require manual loading. In either case, when Angular starts it immediately creates the rootScope, which it attaches to the element with the ng-app attribute, and then starts the compile service. The compile service treats that same element as the document root, and begins its work from that point.
The compile service is a two step process. More importantly, the compile service is also the point at which directives are initialized. As elaborate as it seems, a directive is just a function executed when the compiler service reaches it in the DOM. The first step the compile service takes is – appropriately – to compile each directive it finds. Specifically the compile service runs the directive’s own compile function. If the compile service finds an element with more than one directive, then the directives are sorted by priority and each compile function is executed in order.
Parsing the entire DOM for directives and executing them in turn is not terribly performant. The responsiveness of an application with thousands of directives would be impacted by this type of recursive process. Therefore, Angular separates the effort of directive compiling with the effort of linking the compiled directive to a scope and attaching the necessary listeners / watchers. This second effort is step two in the two step compile service process. Essentially the link step produces a live binding between the DOM and the scope. Remember also, the scope in this instance is the scope as defined by the directive – not necessarily the scope of the immediate parent controller. More on that below.
How Directives Work
Angular provides a special augmentation to the browser’s event queue called the digest loop. In the normal event loop, events occur and are placed into the queue for execution. If function handlers are configured to execute when an event fires they are called with the event as a parameter. Thanks to the digest loop, directives themselves can register event listeners So when a given event fires, the directive runs – within the context of the digest loop.
So what does this mean from a practical standpoint? When a browser event triggers, if the directive is listening it fires – in the digest loop – and the digest loop will keep spinning until all possible changes spawned by the directive are resolved. In fact, once the final change is made the loop will run one last time, just to be sure. If you have ever seen duplicated console.log() messages, this is the reason.
What makes Angular so powerful is that the digest process occurs between every browser event. This means directives registered as event listeners react and behave just like built in functions, and can be as performant as other, non-Angular techniques such as those favored by jQuery.
The DOM and the Directive
Unlike other MVC frameworks, Views in Angular are constructed with plain old HTML syntax. Unlike jQuery, Angular does not explicitly manipulate the markup within a View to provide user interface behavior. Views in Angular are the official record, the final statement clearly documenting what happens upon rendering. As a result, Angular rarely requires element IDs, and classes are used almost exclusively for styling.
Standard jQuery programs take an imperative approach to DOM manipulation. Elements intended as part of some user interface behavior are commonly configured with IDs, and the developer crafting the interaction must be well aware of the entire DOM. In contrast, Angular’s declarative approach requires the developer to handle only the data pushed into the View by the Model. The plumbing is handled by the framework, behind the scenes.
Directives are an important part of Angular’s declarative approach. A directive is literally an improvement to the DOM – an extension of what ordinary markup is capable of, while obeying HTML syntactical rules. A well crafted directive can be shared between projects – consider the large number of directives provided by Angular. For this reason, best practice – aka “The Angular Way” – is that developers stuff all DOM related functionality into a directive or set of directives, and to keep this functionality as declarative as possible.
A Peek Inside The Machine
A directive contains two basic pieces: the return object and all of the code outside of the return. The return object itself possesses many different properties and methods. However a directive requires surprisingly few properties to operate correctly. For example:
angular.module('app.directives') .directive('spinner', function(){ return { restrict: "E", template: "<div id='circularG'> … </div>" }; });
Note how the restrict and template properties require strings. Some properties can accept multiple data types. The scope property, for example, accepts true/false. It can also accept an object as in this example:
angular.module(app.directives') .directive('activateAndToggle', function() { var YES_CLASS = "upsellBtnYes"; var NO_CLASS = "upsellBtnNo"; return { restrict: 'A', scope: { answerButton: '@', toggleClick: '&' }, link: function(scope, element, attrs, ctrl) { element.bind('click', function(evt) { … }); } } });
Also, notice the variables declared prior to the return. These variables are useful later within the link’s click listener – and declaring them outside the return improves overall code reusability.
Here are a few of the more common properties and methods available to the return object:
Restrict | A property that defines how a directive is allowed to appear within the DOM. The restrict property will accept one or more items from this list:
E – Element ( <my-directive></my-directive> ) |
Scope | Property that determines the relationship between the directive’s scope and the immediate parent’s scope. The options are:
False – directive scope is an extension of the parent’s scope The final choice is to pass the scope property an object, which creates an isolate scope cut off completely from the parent scope. |
TemplateUrl (or) | Markup template required by a directive can be stored in a separate file and loaded into the directive via this property. This template is cached by Angular’s $templateCache service the first time it is loaded, and is accessible through multiple means. Templates cached by $templateCache load very quickly. |
Template | Markup can also be fed to the directive via this property. The markup is part of the overall code structure in this situation, and the markup must be encapsulated by a container (sibling elements without an immediate parent are not allowed). Makes the source code messier, but this property is sometimes easier to use. |
Controller | The controller method accepts parameters via traditional dependency injection. However any code located in the controller is executed BEFORE the directive compiles. Generally speaking it is a better idea to place code within the link function. |
Link | Any behavior interacts with the DOM goes into this method. Link is executed AFTER the directive compiles. Parameters passed to the link are not dependency injections (as with the controller), but they must be passed in a specific order. The parameter definitions are:
scope – directive’s scope object |
Building Community Best Practices
Thanks to Angular’s accelerating popularity, community best practices are forming and solidifying. Adhering to community best practices improves the community and helps you and your organization by ensuring your project stays maintainable and extensible. A couple of our favorite best practices include:
Invoke directives sensibly. If you are building a clock, stock ticker or animated logo widget, use an element. On the other hand, if you are developing specific programmatic functionality use an attribute. The majority of Angular’s bundled directives are invoked as attributes. As for classes and comments? Forget about them. Using elements and attributes make it easier to determine what directives match a given element.
Prefer the templateUrl property over template. HTML template files injected via templateUrl can be cached – or even precached – within Angular’s $templateCache, which eliminates additional XHR transactions thereby improving performance. Additionally, keeping directives free from random string concatenated HTML will improve everybody’s day.
Final Thoughts
Directives are not as bizarre as they may first appear. If you have trouble getting your head around them, try writing your own. Create an event listener on a button using Angular’s declarative approach and prove it to yourself – and then build on that success. Practice by writing your own directives whenever possible. Eventually, these concepts will click – good luck!
Jay Garcia
Related Posts
-
AngularJS: Tricks with angular.extend()
AngularJS has this built-in method for doing object to object copies called angular.extend(). It is…
-
Angular + React Become ReAngular
Angular and React developers have revealed this morning that they are planning to merge to…