Using Custom Directives in AngularJS

One of the areas I found the most confusing when starting out with Angular was directives. However, once I got comfortable with directives I found it to be true what most articles on directives were saying – they are really powerful and can really clean up your code.

Highlighting the active tab for the view

Today I came across this blog post which creates a navigation controller in order to conditionally add an ‘active’ class to the active tab.

app.controller('NavCtrl', function($scope, $location) {
    $scope.isActive = function(route) {
        return route === $location.path();
    };
});
<ul class="nav navbar-nav">
    <li ng-class="{active: isActive('/profile')}">
        <a href="#/profile"><i class="fa fa-dashboard"></i> You</a>
    </li>
    <li ng-class="{active: isActive('/find')}">
        <a href="#/find"><i class="fa fa-bar-chart-o"></i> Find Friends</a>
    </li>
    <li ng-class="{active: isActive('/network')}">
        <a href="#/network"><i class="fa fa-table"></i> Network </a>
    </li>
    <li ng-class="{active: isActive('/chat')}">
        <a href="#/chat"><i class="fa fa-edit"></i> Chat Room </a>
    </li>
</ul>

You can see this example in action on Plunker.

This approach uses the built-in ng-class directive in combination with an isActive method on the scope in order to highlight the active tab for the view. While this certainly works, it’s not the ideal approach. A much cleaner approach (in my opinion) is to write a custom directive which simply adds the active class for us.

Using a custom directive

Let’s try to reproduce this logic with a custom directive.

var app = angular.module('plunker', []);

app.directive('myActiveLink', function($location) {
  return {
    restrict: 'A',
    scope: {
      path: "@myActiveLink"
    },
    link: function(scope, element, attributes) {
      scope.$on('$locationChangeSuccess', function() {
        if ($location.path() === scope.path) {
          element.addClass('active');
        } else {
          element.removeClass('active');
        }
      });
    }
  };
});
<ul class="nav navbar-nav">
    <li my-active-link="/profile">
        <a href="#/profile"><i class="fa fa-dashboard"></i> You</a>
    </li>
    <li my-active-link="/find">
        <a href="#/find"><i class="fa fa-bar-chart-o"></i> Find Friends</a>
    </li>
    <li my-active-link="/network">
        <a href="#/network"><i class="fa fa-table"></i> Network </a>
    </li>
    <li my-active-link="/chat">
        <a href="#/chat"><i class="fa fa-edit"></i> Chat Room </a>
    </li>
</ul>

You can see this example in action on Plunker.

Breaking down the directive

Let’s break down the directive to see what exactly is happening. To create a directive we simply call the directive function with a function which returns an object literal. There are a whole host of options which can be specified – look at the $compile documentation for a full reference.

You will also notice that I named my directive as ‘myActiveLink’, but I am specifying the html attribute to invoke the directive as ‘my-active-link’ – this is the angular convention which you need to use.

Let’s break down the options I am passing, line by line.

restrict: 'A'

Restrict specifies where the directive can be used – in this case the directive can only be used as an attribute (the most common option). Other options are E (element name), C (class) and M (comment). You can also combine this, so AC would allow the directive to be used as an attribute or as a class.

scope: {
  path: "@myActiveLink"
}

Scope allows us to specify attributes on the element where the directive is declared to be made available on the scope. So here the ‘my-active-link’ attribute (which is also used to declare the directive) will have it’s value copied into a variable on the scope called ‘path’. If both sides of the declaration are equal you can leave off the right side, so I could also have created a scope variable called ‘myActiveLink’ by doing this:

scope: {
  myActiveLink: "@"
}

Here I am using ‘@’ to do the binding, which means I am doing 1-way binding. There are 3 different options for doing the binding – @ (which is one-way binding), = (which is two-way binding) and & (which is used to bind to an expression).

Declaring the scope in this way will also create an isolated scope for the directive – the default for directives is to declare the scope as false, which means the directive will simply inherit the parent scope.

link: function(scope, element, attributes) {
  scope.$on('$locationChangeSuccess', function() {
    if ($location.path() === scope.path) {
      element.addClass('active');
    } else {
      element.removeClass('active');
    }
  });
}

The link function is used to register any DOM event listeners you may need as well as for updating the DOM. From our scope declaration we now know there will be a variable on the scope called path which is the value declared in the view. We now need to listen for changes to the $location.path() value and update our element to reflect this change. By looking at the Angular $location documentation it is easy to see that we can simply subscribe to the $locationChangeSuccess event and update the element accordingly. The last thing to note is that the element passed to the scope is effectively a jQuery object (it’s actually a jqlite object) so you can use most of the functions you are used to in jQuery.

More on directives

Directives are a very powerful tool, once you get comfortable using them. If you are looking for more information on directives you can check out the API documentation, or take a look at Dan Wahlin’s excellent 3-part series on directives. Happy coding.