AngularJS Directives That Override Standard HTML Tags

By Joel Hooks

Directives are the heart and soul of AngularJS. They are incredibly powerful. AngularJS extends the grammar of the browser, supplying semantics that facilitate the creation of web applications that go beyond the standard hyper-linked web page. The primary weapon to accomplish this is the directive.

Creating your own directives is an awesome way to create composed, reusable behaviors in your applications. This article isn’t about making your own directives, instead I wanted to take a closer look at the internal directives of AngularJS. Specifically, we will be looking at how and why AngularJS extends the functionality of native HTML tags like <input> and <a> to make the magic happen. Looking at Angular’s built-in directives can help shed light on the power directives offer.

A is for Anchor

To start, let’s take a look at Angular’s internal htmlAnchorDirective.

var htmlAnchorDirective = valueFn({
  restrict: 'E',
  compile: function(element, attr) {

    if (msie <= 8) {

      // turn link into a stylable link in IE
      // but only if it doesn't have name attribute, in which case it's an anchor
      if (!attr.href && !attr.name) {
        attr.$set('href', '');
      }

      // add a comment node to anchors to workaround IE bug that causes element content to be reset
      // to new attribute content if attribute is updated with value containing @ and element also
      // contains value with @
      // see issue #1949
      element.append(document.createComment('IE fix'));
    }

    return function(scope, element) {
      element.on('click', function(event){
        // if we have no href url, then don't navigate anywhere.
        if (!element.attr('href')) {
          event.preventDefault();
        }
      });
    }
  }
});

The htmlAnchorDirective has a simple job. It is there to prevent navigation and page reloading. Typically this is in conjunction with ng-click, which is used to actually capture the click and navigate the user within the application. Every <a> in your application is effectively extended by AngularJS. The functionality is primarily the event.preventDefault() that is applied if the anchor tag doesn’t have an href attribute.

One thing to note (and this is typical throughout the AngularJS internals) is that this directive requires special attention for IE 7. When I’m digging around in the internals, I’m always appreciative of this effort made by the AngularJS contributors. These aren’t fun problems to solve, and it is nice that somebody has made the effort to solve them for us.

Digging into <form>

AngularJS overrides <form> to provide some important functionality. The core of this extension of <form> is to prevent any page refresh that would occur with an unmodified <form> tag. Lets have a look:

var formDirectiveFactory = function(isNgForm) {
  return ['$timeout', function($timeout) {
    var formDirective = {
      name: 'form',
      restrict: 'E',
      controller: FormController,
      compile: function() {
        return {
          pre: function(scope, formElement, attr, controller) {
            if (!attr.action) {
              // we can't use jq events because if a form is destroyed during submission the default
              // action is not prevented. see #1238
              //
              // IE 9 is not affected because it doesn't fire a submit event and try to do a full
              // page reload if the form was destroyed by submission of the form via a click handler
              // on a button in the form. Looks like an IE9 specific bug.
              var preventDefaultListener = function(event) {
                event.preventDefault
                  ? event.preventDefault()
                  : event.returnValue = false; // IE
              };

              addEventListenerFn(formElement[0], 'submit', preventDefaultListener);

              // unregister the preventDefault listener so that we don't not leak memory but in a
              // way that will achieve the prevention of the default action.
              formElement.on('$destroy', function() {
                $timeout(function() {
                  removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
                }, 0, false);
              });
            }

            var parentFormCtrl = formElement.parent().controller('form'),
                alias = attr.name || attr.ngForm;

            if (alias) {
              scope[alias] = controller;
            }
            if (parentFormCtrl) {
              formElement.on('$destroy', function() {
                parentFormCtrl.$removeControl(controller);
                if (alias) {
                  scope[alias] = undefined;
                }
                extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
              });
            }
          }
        };
      }
    };

    return isNgForm ? extend(copy(formDirective), {restrict: 'EAC'}) : formDirective;
  }];
};

The above function is a factory that creates a form directive. The directive itself does several things. Aside from some memory management it also serves to prevent the default behavior of the form action. Typically within an AngularJS application, you will want to capture the user’s form input and feed that data into a controller to send it to the server. This is different from the standard action attribute of a form that will perform a POST operation and typically redirect the user to a new page. This behavior is probably not what you want in your single-page JavaScript application, so AngularJS is working to help prevent that. You probably still want to be able to submit your form, and the ngSubmit directive placed as an attribute on the <form> tag will execute an expression when your designated submit input is clicked.

If you’re paying close attention, you’ll notice that the form directive above has a FormController assigned to it. The FormController is the brains of all the forms within an AngularJS application, and every <form> gets one. The FormController tracks all of the controls within a form and manages the validity of the form.

The Input Directive

var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
  return {
    restrict: 'E',
    require: '?ngModel',
    link: function(scope, element, attr, ctrl) {
      if (ctrl) {
        (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
                                                            $browser);
      }
    }
  };
}];

Wow! It’s so simple 😉

It is. This is because the actual <input> tag is only an entry point. The real work is done based on the type of input that is being used. AngularJS is looking for the following input types:

  • text
  • number
  • url
  • email
  • radio
  • checkbox

With the URL, email, and number types, AngularJS provides some basic validation:

var URL_REGEXP = /^(ftp|http|https)://(w+:{0,1}w*@)?(S+)(:[0-9]+)?(/|/([w#!:.?+=&%@!-/]))?$/;
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}$/;
var NUMBER_REGEXP = /^s*(-|+)?(d+|(d*(.d*)))s*$/;

Here’s the code for the URL type:

function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  textInputType(scope, element, attr, ctrl, $sniffer, $browser);

  var urlValidator = function(value) {
    if (isEmpty(value) || URL_REGEXP.test(value)) {
      ctrl.$setValidity('url', true);
      return value;
    } else {
      ctrl.$setValidity('url', false);
      return undefined;
    }
  };

  ctrl.$formatters.push(urlValidator);
  ctrl.$parsers.push(urlValidator);
}

Simple stuff. It uses the REGEX above to validate and then sets the validity on the FormController, which you can then use to display feedback to the user. Email and number validation works in a similar fashion.

With text-type inputs AnglularJS also provides data-binding via ngModel, which is an extremely convenient solution to capturing user input and displaying it in the form. I plan to examine ngModel closer in a future post.

Just the beginning.

It wasn’t immediately obvious to me when I started using AngularJS that the framework was overriding these default HTML tags to add the secret sauce on top. Once the realization dawned on me, it opened my eyes to the power and potential that directives hold. You aren’t restricted to the extensions that AngularJS provides with these built-in directives. You can further extend the capabilities of HTML by creating your own directives that override and extend the native HTML elements.

Digging into the guts of the AngularJS source code is a great way to learn the hows and whys of the framework, and can reveal techniques that can be applied to your own applications. The AngularJS source is well documented, cleanly written, and well tested. If you’re working with AngularJS, I highly recommend diving into these internals and discovering this for yourself. It won’t be time wasted.

This article was originally posted at https://joelhooks.com/blog/2013/07/15/a-look-at-angularjs-internal-directives-that-override-standard-html-tags/

Photo Credit: Maximilien Brice and copyright CERN

Previous

Soma.js – Your Way Out of Chaotic JavaScript

DOM Traversal and Manipulation with Voyeur

Next

8 thoughts on “AngularJS Directives That Override Standard HTML Tags”

  1. Hay joel, this is a really nice and useful post. Being an angular user, its g8 to know its internals. Angular made a change in approach of client side development.

    Once again thanks for sharing.waiting for your ngModel post.

  2. Joel,
    While I knew directives allowed you to create custom widgets and attach behaviors to DOM elements, I had not given much thought to the AngJS undercover, default behaviors for standard DOM elements. I have studied the AngJS engine several times [for other concerns] but it is so rich in substance that I missed these aspects.

    Your article revealed to me some `assumed` functionality and again highlighted how powerful directives are. Thank you for a really nice blog tutorial.

    One thing that continues to confuse developers is the differences between directive `compile` and `link` functions. I think an article is needed discussing how the `compile` directive phase allows developers to modify the HTML source before DOM is constructed and the `link` directive phase is used to link the DOM element instances to scope and additional logic.

  3. Yup! I didn’t really “get it” until I saw what directives could do. I had one of those rare epiphanies seeing that much of what I knew up to that point about building web apps was suddenly obsolete. Directives are a paradigm shift.

Comments are closed.