ANGULARJS DIRECTIVE BEST PRACTICES

Using directives in AngularJS is one of the great features added to tie complicated javascript functionality and client-side templating to your HTML app in a way that allows for re-use and maintainable code. You can think of a directive as an extension of HTML so that you can create your own elements and attributes. Just like when you use a “<select>” element, you expect to see an element that you can click on and will drop down to have a list of items, and you expect to be able to click one and have that one show up as the “selected” one.  Where a HTML element implies certain functionality, a directive allows us to create our own display elements and/or functionality that can be used as new elements or to extend existing elements. I’ll use the example of a basketball team where we have many basketball players making up a basketball team.  Wouldn’t it be nice if in your HTML you could write this to display a basketball player in HTML?

<basketball-player></basketball-player>

As you might expect to see in HTML, this would output a basketball player with a few fields like name, number, position, and some stats like points and assists.  To accomplish this, it would take a combination of several HTML elements, including <div>, <span>, and maybe an <input> if we wanted to change the number of points and assists in a scorebook type of app.

For the code examples shown on this page, I'm using AngularJS 1.2.7, they should all work with 1.2.*. Here is a link to a plunker showing an example of how to create your own custom directive to do this: http://plnkr.co/edit/BaR0ua?p=preview The basketballPlayer directive is associated with a template file that is HTML with some AngularJS template markup within. Take a look at how I implemented the capability to change the points using an input field.  I can use a directive as an attribute, called ng-model, which is one of the directives provided by AngularJS core. That’s right, many core AngularJS features are directives themselves, a directive can be an element name or an attribute.  This is why understanding directives is one of the most important things to learn and understand when learning AngularJS. As with any technology, there are many ways to approach a solution, and creating directives is no exception. We have some choices to evaluate, a few things to consider include:

  • Using Attribute vs. Element

  • Using proper HTML5/XHTML5 markup

  • Scope considerations for reusable code

Using the element name

<basketball-player></basketball-player>

may seem like a really cool feature of AngularJS and a huge readability improvement to someone going through source code.  However, this has no chance of validating as proper HTML5. Even though it is not proper HTML5, this is the preferred way to use a directive that adds new markup to your app.  Another wise suggestion is to use a prefix (think namespace) for your directives that you use as an element name, which is mainly a future-proofing in case an HTML element comes out that has the same name someday.  Like maybe you were thinking of:

<multiselect>

But what if an upcoming version of HTML5 adds a special multiselect tag and all of the sudden your app breaks.  A suggestion is to use:

<myapp-multiselect>

That way you know for sure it’ll be OK in the future. But something like:

<basketball-player>

I’m pretty sure the chance of that being used in a future release of HTML is none. Having an element name work as a directive is not enabled by default, you need to explicitly allow for it in your javascript code, I keep all my directives in a directives.js file:

var myDirectives = angular.module('myDirectives', []); myDirectives.directive('basketballPlayer', function() { return { restrict: 'AE', templateUrl: 'bball-player-template.html' }; });

The restrict property has an A for Attribute, and an E for Element.  The default, if you don’t specify a restrict property, is A, which means it’s only enabled for Attributes. Notice the name of the directive is “basketballPlayer” but I called the element “<basketball-player>”.  This is an automatic conversion from camel case to lower case with dashes splitting words, it’s an AngularJS thing that you can’t control.  There are lots of possible options that it gets converted into that are valid that allow you to use it in several ways, some help you out with HTML validation. If you are concerned about HTML validation, you can write it also as an attribute:

<div data-basketball-player>

If you are not familiar with the special attribute prefix of “data-“, this is a special name and is allowed within HTML, any attributes with a prefix of “data-“ are ignored and allowed to be custom attributes.  The attribute-based syntax is equivalent to <basketball-player>. This format is valid HTML5, but if you want to step up to XHTML5 we’re not quite OK yet.  Attributes are required to have a value in XHTML5, so most correctly:

<div data-basketball-player="argument">

Even if you don’t have an argument, it’s appropriate to give it a value, recall from plain old HTML:

<select> <option value="1" selected>My selected option</option> <option value="2">Not selected option</option></select>

Notice the selected doesn’t have an = sign behind it.  This is called Attribute Minification. This is allowed in HTML5, but is not proper XHTML5.  So again, this is up to you and how strict you want to be.  The proper XHTML5 form for this select element is:

<select> <option value="1" selected="selected">My selected option</option> <option value="2">Not selected option</option></select>

While I like being strict, I think it’s silly to write selected=“selected” so I opt for not following XHTML5 if that choice is up to me. Within AngularJS, when you create a directive, it requires a small amount of javascript and a corresponding template.  You can determine whether the directive name you have chosen is allowed to be included in the code as an element or as an attribute, or both.  The default is to only allow it as an attribute.  This is the way I prefer, I like being able to run my HTML code through a validator and have it pass.  This is determined by the restrict property of the directive as mentioned earlier in this article. The AngularJS docs suggest that you use an element when you are creating a component that is in control of the template.  Typical situation for this is in some domain-specific code.  They suggest you use an attribute when you are decorating an existing element. In the real world, when looking at documentation online, and help from Stack Overflow and other sites, you will almost always see developers using the most condensed form, using element names and attribute minification.  Some of this is because they are trying to communicate a solution in the simplest manner, but I also suspect this is what most are using within their apps. In my own code, I like using HTML5 validation because it helps me check out all my HTML to make sure I didn’t miss a close or open tag, but I don’t typically go so far as requiring XHTML5 validation. Scope considerations for reusable code You have the option with a directive to isolate the scope, which means to access any variables inside your directive’s template, you need to pass it into the template, using an attribute. Here is an example of the same code from above in a new plunker showing isolate scope: http://plnkr.co/edit/S441rN?p=preview Notice that the directive now includes an additional attribute:

<basketball-player player="basketballPlayer"></basketball-player>

And the directive code says to grab a player directive:

myDirectives.directive('basketballPlayer', function() { return { restrict: 'AE', scope: { player: '=' }, templateUrl: 'bball-player-template.html' }; });

The = sign says to also assign player attribute to the variable of the same name so inside the template, you use player as the variable name, as you can see in the bball-player-template.html file:

<div class="basketball-player"> <div>Name: {{player.name}}</div> <div>Number: {{player.number}}</div> <div>Position: {{player.position}}</div> <div>Points: <input type="text" ng-model="player.points"/></div> <div>Assists: <input type="text" ng-model="player.assists"/></div></div>

If you need to have some two-way data binding inside of your directive, then there are some situations when you cannot isolate scope.  You can isolate scope and still two-way data-bind when you are only binding to objects, but not primitives.  It’s tricky, so keep it in mind as you design your app. Isolating scope is a really good thing, much cleaner, and leads to decoupled code, passing in the objects you need from the outside and nothing more.  So if you do need to two-way data-bind inside your directive, consider tossing your variables into an object and passing that to your directive so you can still isolate scope.

For some good reading about isolate scope and how it all works along with no scope isolation and the transclude option, check out this stack overflow question: Confused about Angularjs transcluded and isolate scopes & bindings AngularJS Directive Best Practice Guidelines

  1. Use your directive as an element name instead of attribute when you are in control of the template

  2. Use your directive as an attribute instead of element name when you are adding functionality to an existing element

  3. If you do use a directive as an element, add a prefix to all elements to avoid naming conflicts with future HTML5 and possible integrations with other libraries.

  4. If HTML5 validation is a requirement, you’ll be forced to use all directives as attributes with a prefix of “data-“.

  5. If XHTML5 validation is a requirement, same rules as HTML5 validation except need to add “=“ and a value onto the end of attributes.

  6. Use isolate scope where possible, but do not feel defeated if you can’t isolate the scope because of the need to two-way data-bind to an outside scope.