Custom Directives in Angular

Custom Directives in Angular

November 10, 2015

ng-app, ng-view, ng-repeat, ng-if, and all the other ng-somethings are examples of Angular's built-in directives. Directives enhance HTML to do special things that cannot be done with plain old HTML. For instance, adding the ng-if attribute to an HTML element will conditionally render that element if its expression evaluates to true. Likewise, adding the ng-repeat attribute will repeat an HTML element for items in an array. These types of programming features, control flow and looping, aren't inherent parts of HTML. Directives add these extra super powers to your markup. Along with built-in directives, Angular allows you to create custom directives which can make up a large portion of your Angular JS application.

Custom directives allow you to do at least two interesting things:

1 – Create custom event handlers. Click on some element and something happens.

2 – Create new HTML tags to improve semantics, code readability, and the authoring experience.

Take this sample directive that does nothing.

brewitivity.directive();

Let’s say the name of my dummy Angular application is brewitivity so I have a variable with the same name that represents my Angular app (which is why I’m chaining .directive() to it). A directive is a method that I can pass two arguments: first is a string that represents the directive name, and the second is a callback function.

brewitivity.directive("coffeeMaker, function()( {
  //directive code will go here...
});

In this case, coffeeMaker is the name of the directive. An anonymous function is passed as the second argument. I could also easily pass it a named function.

Creating a template

So let’s say we want this directive to be a custom element that is a placeholder for a larger component, the coffee maker component.

Template

brewitivity.directive("coffeeMaker", function() {
  return {
    template: '<div><h2>Coffee Maker</h2><p>Type of roast</p></div>'
  }
});

Inside the callback function we’re returning an object with one property: template. The value of that is any markup we want. So now to connect the dots, we have to add the coffeeMaker directive as an attribute of an element in our HTML.

<!-- index.html -->
<div coffee-maker></div>

Custom Elements

By default, the name of the directive is added as an attribute on an HTML element. However, we can specificy to restrict it to “E” only and this allows us to create a custom element. Typically if I’m just adding an event listener to an existing element, I’ll create the directive as an attribute, but if I’m creating a new module I’ll do it as a custom element. Let’s restrict this directive as an element only.

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    template: '<div><h2>Coffee Maker</h2><p>Type of roast</p></div>'
  }
});

We can also do something like restrict: "AE", which allows your directive name to be used as an attribute (A) on an element or as an element (E) itself. You can use any combination of letters. If you don’t specify the restrict property, its default value is A.

Now our HTML can look like this:

<coffee-maker></coffee-maker>

Or we can simplify it even further to this:

<coffee-maker/>

Either way using an element is a little more straight-forward than using an attribute, however, creating a custom element like this isn’t going to work in IE8 and lower without creating polyfills for those older browsers. If I’m not supporting IE8, which recently I’ve had the luxury of not supporting, I prefer the custom element route as its more readable and just feels good. 🙂

Replace

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    replace: true,
    template: '<div><h2>Coffee Maker</h2><p>Type of roast</p></div>'
  }
});

Since I’m creating a custom element, I’m going to set the replace property to true which will make sure the custom element is not rendered in the DOM. The template I specified will show in its place.

Template URL

Now we should take this one step further and instead of inlining the HTML in the directive let’s instead point it to a separate file.

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    replace: true,
    templateUrl: 'components/coffee-maker.html'
  }
});
//coffee-maker.html
<div><h2>Coffee Maker</h2><p>Type of roast</p></div>

Instead of using template we are now using templateUrl with a value of the partial file location. This is definately a best-practice, especially if you have more than a line or two of markup.

Adding an event handler

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    replace: true,
    templateUrl: 'components/coffee-maker.html',
    link: function(scope, element, attrs, controller, transcludeFn) {
      element.bind("click", function() {
        //do something
      });
    }
  }
});

This is somewhat familiar to jQuery’s common pattern:

$(".something").on("click", function() {
  //do something
});

Let’s come back to that.

The link property gets passed a function that takes up to 5 arguments: scope, element, attrs, controller, and transcludeFn. I have only used element and scope in my travels, but then again I haven’t traveled too far with Angular. These arguments are optional, so you could pass one or none at all. element is particularly useful. It happens to be the element that matches the directive name.

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    replace: true,
    templateUrl: 'components/coffee-maker.html',
    link: function(element) {
      element.bind("click", function() {
        element.addClass("brewing");
      });
    }
  }
});

So when we click on the element we are also adding the class “brew” to that same element. Here’s something: addClass(). This isn’t jQuery but it does the exact same thing as the addClass() method in jQuery.

jqLite

Angular comes standard with something called jQLite, which is a smaller subset of jQuery that has most of the more useful jQuery methods like toggleClass(), find(), html(), attr(), hasClass(), and… you get the picture. Though, not all these methods have the full functionality of the jQuery library. For instance, parent() doesn’t support selectors, so you couldn’t write something like $(this).parent(".the-parent-class-name"). Also worth a mention is that not every jQuery method is in jqLite, for instance, none of jQuery’s ajax features are included, as Angular has its own $http that it uses to ajax stuff on to the page.

jQuery

To keep Angular small, the super powerful jQuery selector engine isn’t included in jqLite either. But if you include the jQuery script right before Angular, then Angular will automatically utilize jQuery. Then we can write something like this:

brewitivity.directive("coffeeMaker", function() {
  return {
    restrict: 'E', //E - Element, A - Attribute,  C - Class
    replace: true,
    templateUrl: 'components/coffee-maker.html',
    link: function(element) {
      element.bind("click", function() {
        angular.element(".roast").addClass("brewing");
      });
    }
  }
});

angular.element(".selector") is simply an alias for $(".selector"). In fact if you’d rather be more succinct you don’t even have to use angular.element(".selector") and you can just go with the more concise $(".selector").

Mixing jQuery and Angular: Try not to cross the streams

The key to using Angular successfully is to not start writing jQuery willy-nilly all over the place. But instead, using jQuery inside a directive to have a more robust selector-engine is usually well within the realm of a good-practice. Using jQuery’s Ajax functions, and manipulating the DOM with jQuery outside of a directive is usually going to be bad practice and will leave you scratching your head as things don’t work as you may have thought they would.

Conclusion

A Sitepoint article noted that, Directives are the most important components of any AngularJS application. We’ve only scratched the surface of using a directive. One day I’ll figure out what the transcludeFn argument does and my status will instantly be elevated to Narly Chief of TranscludeFn-town. I’ll also write a follow up article that goes further in depth on directives once I figure out what all the other stuff does.To recap though, directives do two things: create event handlers and custom elements. Here’s a codepen:

See the Pen Custom Directives 1 by Rich Finelli (@richfinelli) on CodePen.

Mastering CSS: Book by Rich Finelli
Mastering CSS, the Book! Flexbox, Layout, Animations, Responsive, Retina, and more!
Mastering CSS, 2nd Edition, video course by Rich Finelli
Mastering CSS, Second Edition: 47 videos on how to make websites like a boss! Flexbox, Animations, Responsive, Retina, and more!
Back to top