Kurtis with his dog Guinness (Australian Shepherd)
CTO @ Fullscript. Focused on building great teams, culture, and software.

February 2014

Introduction to AngularJS Directives

What are directives, and why should you care about them?

Directives are regularly described as the ability to teach HTML new tricks, but what the heck does that mean?

Let’s imagine a scenario where a directive can be helpful.

You have a contact form on your site where your users can submit their email and hit send. The problem is, your backend is really slow to respond, so users become impatient and press “submit” multiple times, causing multiple requests to be made [and spamming your email!].

Wouldn’t it be nice if the submit button became disabled after the form was submitted, and then re-enabled after the request was finished?

In this tutorial we’ll be building a basic directive to handle this scenario.


Here is the directive in action, try it out!

Save

List of things you've entered:

Pretty cool, right?

Not only do we prevent double form submission, but we also provide some visual feedback to the user (changing “Save” to “Saving…”).

Now that we know what we’re building, let’s hop into it.


The HTML

<!-- Our Form Stuff -->
<form name="myForm">
  <input type="text" ng-model="newThing.content" placeholder="Type something...">
  <button ng-click="submitForm(newThing)" loading-text="Saving..." enable-button="myForm.$pristine" submit-button>
    Save
  </button>
<form>

I’ve removed all unneccessary css styles and markup so that we’re just looking at the code necessary for the directive. Full source is available here

<form name="myForm">: Form is not only a default HTML tag, but also a directive in AngularJS. By assigning a name to our form, we have access to myForm from within our controller’s $scope. This gives us access to certain methods and properties, such as $valid to check for validity.

<input>: let’s take a look at the input tag. Here we are binding the text field to newThing.content, which will assign it to $scope.newThing.content in our controller. This is the object that we will be submitting with our form.

<button>: now for the fun stuff, the button. There are a few things to explain here before diving into the JavaScript.

<button ng-click="submitForm(newThing)" loading-text="Saving..." enable-button="myForm.$pristine" submit-button>
  Save
</button>
Attribute Description
ng-click="submitForm(newThing)" This will call the submitForm() function in our controller when clicked, and pass in the newThing object, that we have altered within our form.
loading-text="Saving..." This is the text that will replace "Save" after the button is clicked.
enable-button="myForm.$pristine" This is passing in the state of the form to our directive. The method `$pristine` returns true if the user has not altered the form yet.
submit-button By adding this as an attribute to our button element, we are instantiating our submitButton directive.

Now that we have taken a look at the HTML markup needed for our form, let’s move on to the JavaScript…


The JavaScript

// Our ng-app and controller
app = angular.module('MyApp', []);

app.controller('MyCtrl', function($scope, $timeout) {
  $scope.listOfThings = [];
  $scope.newThing = {};

  $scope.submitForm = function(thing) {
    // Adding a timeout here to simulate
    // a very long request to backend.
    $timeout(function() {
       $scope.listOfThings.push(thing); // Add to listOfThings
       $scope.newThing = {}; // Reset newThing to empty object
       $scope.myForm.$setPristine(); // Reset the form so myForm.$pristine is true
    }, 1500);
  };
});

Here we are just creating the application called MyApp, and a basic controller. MyCtrl contains an array, listOfThings, and the submitForm() function that is called when pressing Save. I have added a 1.5 second timeout to simulate a slow response from a web server.

Our Directive: submitButton

app.directive('submitButton', function() {
  return {
    restrict: 'A',
    scope: {
      loadingText: "@",
      enableButton: "="
    },
    link: function ($scope, ele) {
      var defaultSaveText = ele.html();

      ele.bind('click', function(){
        ele.attr('disabled','disabled');
        ele.html($scope.loadingText);
      });

      $scope.$watch('enableButton', function() {
        ele.removeAttr('disabled');
        ele.html(defaultSaveText);
      });
    }
  };
});
Code Description
app.directive('submitButton', function() {
  // ...
});
Creates the directive named submitButton. You probably noticed in our HTML that we added an attribute called submit-button to the element. Directives are automatically converted from hash-style in HTML, to camelCase in our JavaScript.
ex: submit-button #=> submitButton
restrict: 'A',
States that this directive can only be added as an attribute on an HTML element. By default, all directives are restricted to attribute only. If you wanted to create a custom element like <submit-button></submit-button>, you would restrict to an element ('E').
scope: {
  loadingText: "@",
  enableButton: "="
},

Sets the directive to have an isolated scope. This means that the directive has it's own $scope, and it will not have access to the properties/methods that are defined in MyCtrl. This is often considered to be a best practice, as the directive is not dependent on belonging to a specific controller.

This also sets two variables within our directive. Let's look into what the @ and = signs mean, and how they were set.

  • loadingText: "@" - This is passed in by our button by setting the attribute loading-text="Saving...". The @ sign means that this is passed in as a string value.
    If we set a variable in MyCtrl like this: $scope.savingText = "Blah", and we wanted to pass the string value to our directive, we would pass it in like this: loading-text="{{savingText}}" - because the attribute is not evaluated by the directive, it expects to receive a string.
  • enableButton: "=" - Passed in by the attribute enable-button="myForm.$pristine". The = sign means that this will be evaluated before being passed in. In this case this will be a boolean, true or false, depending on our form state.
link: function ($scope, ele){
  // ...
}
The link function is where the magic happens. Here is where the logic of the directive is written. You have access to the $scope, element, and attributes (not shown here).
ele.bind('click', function(){
  ele.attr('disabled','disabled');
  ele.html($scope.loadingText);
});
Here we are binding the click event to a function that will disable the button, and change the text to the loading-text value that we passed in earlier ('Saving...')
var defaultSaveText = ele.html();
// ...
$scope.$watch('enableButton', function() {
  ele.removeAttr('disabled');
  ele.html(defaultSaveText);
});

First we are grabbing the original button value, "Save", and storing it in the variable defaultSaveText.

Next we are watching the value of enableButton, which is the form state. When the enableButton variable changes, we are enabling the form again by removing the disabled attribute, and resetting the text.

That’s it! Hopefully this has given you an idea of how to contruct a basic directive. Directives can be used in many situations, and can really help to keep your code DRY.

If you’re looking for more in-depth information , please consult the official angular documentation. They have examples on the different capabilities of directives, such as using templates, custom DOM elements, and shared scopes.

© Kurtis Funai 2021