AngularJS Directives Tutorial

In the very first of our series, we decided to focus on a majorly misunderstood and highly complicated (atleast at first glance) part of AngularJS. With our AngularJS Directives Tutorial, we focus on answering simply what Directives are, some examples of already existing inbuilt AngularJS directives, some pro tips and gotchas, before proceeding to walking you step by step through a simple Rating Directive built from scratch.

What are Directives?

Directives in AngularJS are used to make custom HTML elements and simplify DOM manipulation. They can modify the behavior of new and existing DOM elements, by adding custom functionality, like a datepicker or an autocomplete widget. AngularJS comes with its own set of built-in directives, as well as the ability to add your own ones.

Why are directives used in AngularJS?

Standard HTML has elements like <span>, <input>, and <button> that have fixed behavior. To make the <input> element behave like a datepicker, it takes custom CSS and JS calls from your Javascript code. AngularJS makes this easy by allowing you to wrap all this in what we call a directive. So instead of writing

<input id="datepickerElem">

and then calling

$('#datepickerElem').datepicker()
from some random place in your code, you can now instead write, directly in your HTML

<input datepicker>

or

<datepicker>

How to use directives in AngularJS?

Using directives in an AngularJS application is fairly easy. You just need to (usually) add the directive name in whatever DOM element you want right in your HTML. For example, a datepicker Directive, depending on how it is defined, could be used as

<input datepicker>

or

<datepicker>

While declaring an AngularJS directive, the naming convention followed is camelCase. For example, we would define the name of the directive as ‘fundooDatepicker’. But when you actually use the directive in your HTML, it is the dash-separated version. That is, our widget would be ‘<fundoo-datepicker>’ and not ‘<fundooDatepicker>’
To make your directives and attributes HTML5 compliant, you can prefix all AngularJS attributes with “data-” or “x-“. That is, “x-ng-show” or “data-fundoo-datepicker”, and AngularJS will treat it just like you wrote it like a normal HTML element.

AngularJS’ in-built directives

AngularJS comes with a whole set of directives, including ngHide / ngShow, ngClick, ngRepeat and many more. It’s also great in that the AngularJS inbuilt directives use the same API that we will use to write our own directives, so you know, right off, how powerful it can be. The sky is the limit!

Custom Directives – AngularJS Directives Tutorial

Writing a custom directive is fairly easy in AngularJS. We’ll see how to write one by writing a small directive for displaying and changing ratings with stars.

angularjs directives tutorial screenshot

 Step 1 – Add the trivial fundoo-rating directive, and use it from the html

In our first step we will just initialize the rating directive and use it but without displaying anything.

angular.module('FundooDirectiveTutorial', [])
  .directive('fundooRating', function () {
    return {
      restrict: 'A',
      link: function (scope, elem, attrs) {
        console.log("Recognized the fundoo-rating directive usage");
      }
    }

The directive() function is used to create a directive named ‘fundooRating’. The directive returns an object (called the directive definition object) in its callback function. The ‘restrict‘ option is used to specify whether the directive will be used as an element, attribute or a class. The valid values for ‘restrict’ are

  • ‘A’ – Attribute (You want to use your directive as <div rating>)
  • ‘E’ – Element (Use it as <rating>)
  • ‘C’ – Class. (Use it like <div class=”rating”>)
  • ‘M’ – Comment (Use it like <!– directive: rating –>
It is however recommended to not use a directive as an element as certain older browsers like IE or earlier can not recognize custom elements. More details about AngularJS and IE Support can be found here.

The html for the directive can be written as

<div fundoo-rating></div>

The github link for Step1 is over here.

Step 2 – Show filled stars based on bound rating value

In our second step we display the stars as filled based on the bound value passed to the directive. Our directive would now look like this,

directive('fundooRating', function () {
    return {
      restrict: 'A',
      template: '<ul class="rating">' +
                  '<li ng-repeat="star in stars" class="filled">' +
                      '\u2605' +
                  '</li>' +
                '</ul>',
      scope: {
        ratingValue: '='
      },
      link: function (scope, elem, attrs) {
        scope.stars = [];
        for (var i = 0; i < scope.ratingValue; i++) {
          scope.stars.push({});
        }
      }
  }
});

We just added two new fields to the directive definition – the scope and the template. The template field specifies the template that will be used by the directive and scope field specifies the variables that will be on the scope of the directive. The ‘=’ for ratingValue indicates that ratingValue expects an object from the directive.

There is also another variable on the scope called stars. We have not declared it inside the scope object because only those variables that need to passed in from the HTML are specified there.

The link() function still does not do anything except traversing through a loop and push empty objects inside the stars array.

The html will now change to this,

<body ng-init="rating = 5">
  Rating is {{rating}} <br/>
  <div fundoo-rating rating-value="rating"></div>
</body>

The ng-init directive initializes a variable ‘rating‘ with the value 5, which is then passed to the directive’s rating-value field on the scope.

The github link for Step 2 is here.

Step 3 – Set max rating, and show filled stars as per ratingValue

Till now, we’ve covered how to use a directive to display rating stars on the screen. But these ratings just show as many stars as the ratings. What we ideally want are unfilled stars to reflect the maximum rating, and filled stars to reflect the real rating. So let us see how we would add that.

To do that we’ll have to add some functionality to our link function. So here we go,

link: function (scope, elem, attrs) {
   scope.stars = [];
   for(var i = 0; i < scope.max; i++) {
      scope.stars.push({filled: i < scope.ratingValue});
   }
}

Here, every element of stars array has an object with value ‘filled‘ that is evaluated to true or false based on the value of scope.ratingValue that we get from the DOM. But wait a minute, did you notice the scope.max in the for loop? The scope.max is variable is declared on the scope to get the maximum value of the rating passed in through the HTML.

scope: {
   ratingValue: '=',
   max: '='
}

The html will now change to,

<div fundoo-rating rating-value="rating" max="10"></div>

The github link for Step 3 is here.

Step 4 – Change rating whenever ratingValue variable changes

But this is still a very static directive. Till now the directive is just taking an explicit value set by the ng-init directive. What if the value of the rating variable changes? Our directive won’t change to reflect that. So let us see how we would go about ensuring that our directive responds to changes in the variable.

link: function (scope, elem, attrs) {
   var updateStars = function() {
      scope.stars = [];
      for(var i = 0; i < scope.max; i++) {
         scope.stars.push({filled: i < scope.ratingValue});
      }
  };

  scope.$watch('ratingValue', function(oldVal, newVal) {
     if(newVal) {
       updateStars();
     }
 });

Now, we have wrapped our for loop inside a function called updateStars(), and added an angular $watch method. The $watch() is a method on the scope that is used to watch the state of an objects on the scope. Here, the $watch method detects changes in the value of scope.ratingValue object, and it executes the updateStars method when it detects a change.

Now, let us add a button to our UI to change the value of rating after the initialization:

<div fundoo-rating rating-value="rating" max="10"></div>

<button ng-click="rating = 7">Change rating to 7</button>

Now, since the ‘rating’ variable is bound to the ratingValue object, it immediately triggers the $watch() function with a new value which then executes the updateStars() function to update the number of stars to be shown as filled.

The github link for Step 4 is here.

Step 5 – Add ability to click and change a rating

Great, so we have a rating widget that changes the rating when its binding model value changes. But we still can’t click on it to change or submit a rating. How do we do that? It’s simple, we add click event listeners to our directive. The only minor change we’ll need to make is add a function to the scope in the link function to handle the click and call it from inside the template as follows,

In the template make these changes,

<li ng-repeat="star in stars" ng-class="star" ng-click="toggle($index)">

In the link() function add a toggle() function,

scope.toggle = function(index) {
   scope.ratingValue = index + 1;
};

In the above example the list item calls the toggle function when clicked along with the $index value which is nothing but the index of the list element inside the repeater. The toggle() function has to be on the scope for it to be accessible from the template. The toggle() function increments or decrements the value of the scope.ratingValue according to the index. This triggers the $watch() function which then updates the state of the stars.

The github link for Step 5 is here.

Step 6 – Add ability to make ratings widget readonly

What if I wanted my directive to be read only?  i.e, Some ratings should only be viewable, but should not allow users to change the rating. We can add that pretty easily as follows:

First, we need to add another field to the scope object called readonly:

scope: {
   ratingValue: '=',
   max: '=',
   readonly: '@'
}

We have declared readonly with ‘@’ to accept a string value instead of an object value. So our html for the directive would change like this,

<div fundoo-rating rating-value="rating" max="10" readonly="true"></div>

But there’s something amiss, how will my directive know that it has to disable the onclick of ratings directive. To achieve that we have to make changes to our toggle function like this,

scope.toggle = function(index) {
   if(scope.readonly && scope.readonly === 'true') {
      return;
   }
   scope.ratingValue = index + 1;
};

Here, the toggle function will only change the value of scope.ratingValue if readonly is not set to true.

The github link for Step 6 is here.

Step 7 – Add rating selected callback

We now have a fully working rating widget, that can show ratings, respond to changes and handle user clicks. This completes for the most part our AngularJS Directives Tutorial. But how about we add one last feature to it as a bonus? Let us add an event listener that gets triggered whenever the user selects a rating. This could be used to handle logic specific to a controller, like possibly saving the rating to a server, etc. Let us take a look how we would accomplish this:

We first add a function binding to the directive definitions scope parameter, called “onRatingSelected” as follows:

scope: {
      ratingValue: '=',
      max: '=',
      readonly: '@',
      onRatingSelected: '&'
    }

The ‘&’ symbol tells  onRatingSelected  to expect a function . The html for this would be,

<div fundoo-rating rating-value="rating" max="10" on-rating-selected="saveRatingToServer(newRating)"></div>

But wait, from where is the saveRatingToServer function coming from? Since onRatingSelected accepts a function, it has to be passed a function which is on the scope of the Controller where the HTML resides.

So next, we’ll define the controller that has a function called saveRatingToServer() and the rating variable which we were initializing with ng-init till now. The code for the controller is as follows,

 controller('FundooCtrl', function($scope, $window) { 
   $scope.rating = 5; 
   $scope.saveRatingToServer = function(rating) { 
       $window.alert('Rating selected - ' + rating); 
   }; 
 })

Now the only thing left is to call the onRatingSelected() function from inside the directive. And what better place to call it from inside the toggle function where we are handling our click events.

scope.toggle = function(index) {
    if(scope.readonly && scope.readonly === 'true') {
       return;
    }
    scope.ratingValue = index + 1;
    scope.onRatingSelected({newRating: index + 1});
};

One thing extremely important to note over here is that the object passed as argument should have the same key values (i.e. newRating) as they were passed in the html call to the function. So if you had called the sendRatingToServer() function as sendRatingToServer(rating, stars) it needs to be called in the directive with the same keys i.e., scope.onRatingSelected({rating: index+1, stars: 5});

You can add some more stuff inside the sendRatingToServer() function, like sending the ratings value to a real server.

The github link for Step 7 is given here.

That’s It Folks!!

This completes our AngularJS directives tutorial. You can now create your own custom directives and do some really awesome Angular stuff or you can fork our repository on github and add modifications (like showing 1/2 stars) of your own to the ratings directive.

The Demo for the rating directive is given below,

The github link for the entire rating directive is here.

Abhiroop PatelAngularJS Directives Tutorial
  • Pingback: AngularJS Directives Tutorial - Fundoo Solutions : Fundoo Solutions()

  • Pingback: Learning AngularJS – best resources to learn angularjs for beginners. | Bhavin Patel()

  • Md. Shohel Rana

    nice article, but all github link does not work.

    • http://softvar.github.io/ Varun Malhotra

      Yep, links are broken{they don’t even exist). Try visiting the github repo, and browse commits or releases to view step-by-step.

  • http://softvar.github.io/ Varun Malhotra

    Nice Post!

  • http://www.sameerast.com/ Sameera Thilakasiri

    Ya want to thanks, nicely done.

  • Christopher McKee

    Nice article, but in your listener for $watch, the parameters are out of order. The listener for watch is expecting a function of the type: function (newValue, oldValue, scope) as shown at https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch

  • someone901

    I’m kind of a noob. How would you display an average of all ratings given by users?

    • http://theshyam.com/ Shyam Seshadri

      THe way to think about this would be to calculate the average rating value in the controller, and pass that value to the directive using rating-value attribute on the directive

  • Maytham Fahmi

    Great post thx,
    I want to change the image for star to some thing else! Any idea how to do it?

    • http://theshyam.com/ Shyam Seshadri

      The image is coming from u2605 in the HTML for the rating widget. YOu can replace that with an icon or an image tag to see something else instead of the stars