This is a preview-quality chapter of our continuously deployed eBook on AngularJS for .NET developers. You can read more about the project at http://henriquat.re. You can also follow us on twitter (Project: @henriquatreJS, Authors: @ingorammer and @christianweyer)

 

AngularJS Directives - Domain-Specific Extensions to HTML

Domain-specific extensions to HTML? That's quite a statement, given that HTML5 is going through a multi-year review and improvement process. And of course, we're not talking about extensions to the HTML5 specification itself - instead, AngularJS allows you to define custom HTML tags which are then translated to "regular" HTML/CSS/JS on the fly whenever they are rendered in a browser.

This translation step allows you to create simple, domain-specific and semantically expressive HTML. That is: HTML, which expresses the requirements of your application much more clearly than regular HTML might be able to do.

For a quick sample, let's again look at the HTML which we've shown in The two minute guide to AngularJS for .NET developers. In this example, we've used HTML like this to generate a tabbed GUI:

<div ng-app="myApp" class="myApp">
    <tabs>
        <pane title="First Page">
            This is some <i>content on the first page</i>.
        </pane>
        <pane title="Second">
            And this is the second page.
        </pane>
    </tabs>
</div>

In this and the following sections, we'll walk you through the steps of creating custom directives like this to enable you to create expressive HTML.

Starting Simple

Let's just assume that you have an HTML structure with widgets which need a title. The target structure should look like this:

<div class="widget">
    <div class="title">
        This is my widget's title
    </div>
    <div class="content">
        This is some content of my widget ...
    </div>
</div>

With some CSS styling behind the scenes, you then expect the result to look more or less like this:

Even though the CSS classes widget, title, and content provide the necessary styling, the HTML code somehow still does not feel semantically rich. In fact, if you'd later decide to change the concrete HTML representation of a widget (for example, by adding icons to each widget's title), you would need to change all places in your application in which you've added similar widget HTML code.

What you might like to write instead is HTML markup similar to the following:

<my-widget title="Another Widget">
    This is some content of my other widget ...
</my-widget>

And this is exactly what an AngularJS directive allows you to do: define custom HTML, introduce a custom tag my-widget and have it translated to the nested <div>-structure presented above.

As a first step before implementing this directive, you have to ensure that the AngularJS application has a defined name. You can do this by wrapping the root of the application with <div ng-app="myApp"></div> instead of the generic <div ng-app>. This allows you to register custom directives which are then scoped to the application myApp. (You can read more about this way of structuring modules and applications at Modularizing AngularJS Applications).

After this step, you can register the particular application with Angular by using angular.module('myApp', []). You can then register the directive itself with the application by using the method directive() which is called on the application's module. (Please note that AngularJS will map the naming conventions from HTML's my-app and my-widget to JavaScript's myApp and myWidget).

angular.module('myApp', []).
   directive('myWidget', function () {
      return {
         restrict: 'E',
         replace: true,
         transclude: true,
         template: '<div class="widget">' +
            '<div class="content" ng-transclude></div>' +
            '</div>',
         link: function (scope, element, attrs) {
            element.prepend("<div class='title'>" + attrs.title + "</div>");
         }
      };
   });

As you can see, a directive is simply implemented as a function which returns an object. The returned object defines the look and feel of the directive. So let's examine the fields of this object in some more detail:

         restrict: 'E',

The field restrict defines the type of HTML element which can act as a trigger for the directive:

  • E: Element - the directive is used as an HTML tag
  • A: Attribute - the directive is used as an HTML attribute
  • C: Class - the directive is used as a CSS class (in an element's class="..." definition)

For now, we'll define the directive as a custom HTML tag, but you can read more about the other types of directives at Valid HTML with Directives (Please note: this chapter has not yet been released).

   replace: true,

The second field, replace: true, specifies that we want to completely replace the existing HTML with our directive's content. If we would specify false here, we could instead modify the existing HTML element. You would for example use this approach if your custom directive is created as an HTML attribute with restrict: 'A'. In this case, you will usually simply augment the existing HTML element instead of creating a completely new one. (After all, your directive is just an attribute for an existing HTML tag).

   transclude: true,

Transclusion is an interesting concept with directives: It means that the original inner content of the directive's HTML markup (in our case the widget's content) will be transferred/included in the destination markup. This works in conjunction with the attribute following field template which specifies the desired target HTML and which defines the target location for the transclusion:

    template: '<div class="widget">' +
        '<div class="content" ng-transclude></div>' +
        '</div>',

The field template specifies the HTML content which should be the in-browser HTML result of the directive. The attribute ng-transclude is placed on the HTML element which should become the parent of the original content (if transclude is set to true).

The Link Function

If we would use the directive like this, it would already include the widget's content inside of a <div> with class content which would be inside of another <div> with the CSS class widget. It would not, however, display the widget's title. To display the title, we also need to provide a link function which allows for flexible post-processing of the HTML element:

     link: function (scope, element, attrs) {
        element.prepend("<div class='title'>" + 
            attrs.title + "</div>");
     }

The optional link function is called by Angular after the content of template has been used to create a new HTML element and after the transclusion has been performed. In it, you have access to the scope in which the directive is used. If required, you can use this object to implement data-binding operations for custom UI elements. (You can read more about this in Advanced Directives - Combining AngularJS with Existing Components).

Inside the link function, you also - and this might be the most important use case - have access to the jQuery-style element parameter which contains the prepared HTML element and which you can use to modify its DOM structure.

The final parameter of the link function, attrs, gives you access to the directive's container HTML element's attributes. In our example, we use attrs.title to get access to the title attribute of our <my-widget> HTML element. When then construct a new fragment around this title and use prepend to add the title-<div> as the first child of our widget.

Please note: The link-function will always act on a pre-parsed HTML element. If this does not give you enough power or flexibility for your use-case, you can read more about the basically unlimited power of the compile function in Advanced Directives - Combining AngularJS with Existing Components.

Nesting Directives

An interesting aspect of directives which use transclusion - as in the example above - is that they automatically allow the nesting of other directives inside them. You can for example create HTML like the following to display an inner widget inside of an outer 'container'-widget:

<my-widget title="Outer Widget">
    This is some content of my outer widget. <br>
    As part of it, it contains an inner widget.
    <my-widget title="Inner Widget">
        This now is the content of my inner widget ...
    </my-widget>
</my-widget>

When executing this example, you will see that the two elements are now contained inside of one another:

Angular Itself - Examining ng-click

One of the interesting bits and pieces of AngularJS is the fact that its internal framework elements are based on the same mechanisms which are available to you as an application-developer as well. One of the directives you've quite likely encountered before is ng-click, so let's examine it in some more detail.

ng-click attaches an onclick-handler to a HTML element. When the element is subsequently clicked by the user, the specified method in the controller is called. In the following example, we will expose a method increment() on the current scope:

angular.module("myApp", []);

function SumController($scope) {
   $scope.someData = {
      sum: 42
   };

   $scope.increment = function (valueToAdd) {
      $scope.someData.sum = $scope.someData.sum + valueToAdd;
   }
}

The markup which uses this controller will reference {{someData.sum}} to display the value and will bind ng-click to a call to increment:

<div ng-app="myApp" ng-controller="SumController">
    Current Value: {{someData.sum}}
    <br>
    <button ng-click="increment(2)" >Increment by 2</button>
</div>

When you run this example, you'll see that the counter will increase by 2 whenever you click the button:

This is the default behavior of the ng-click directive. So let's just imagine that we'd like to create a similar directive, but also show a JavaScript alert before performing the action. (Yes, I admit that it's quite a contrived example for now, but you'll see a more realistic confirmation popup-directive in a later chapter on modularization of Angular applications).

So let's say that we want to create a directive with the attribute my-confirm-click. (You might wonder about the ng- prefix vs. the my- prefix: the convention is that ng- is reserved for Angular itself, whereas you should pick your own prefix for custom extensions. I would actually recommend that you do not use my- but something more unique, maybe your or your company's initials or something similar, in your real code.)

The easy step for this directive is the HTML code. We have simply replaced the reference to ng-click with my-confirm-click.

<div ng-app="myApp" ng-controller="SumController">
    Current Value: {{someData.sum}}
    <br>
    <button my-confirm-click="increment(2)" >Increment by 2</button>
</div>

We then create the directive and attach it to the module myApp. The directive is marked with restrict:'A' because we want it to be used as an HTML attribute:

var module = angular.module("myApp", []);

module.directive("myConfirmClick", function ($parse) {
   return {
      restrict: 'A',
      link: function (scope, element, attrs) {
         var parsed = $parse(attrs.myConfirmClick);
         element.bind("click", function () {
             alert("Handling the click!");
             scope.$apply(function(){
               parsed(scope);
            });
         });
      }
   }
});

The important part in this directive is the link-function. The injected $parse-function compiles an expression (like our attribute's textual value "increment(2)") to an accessor function which can later be used to interact with the underlying scope object.

We then bind to the element's click event to be notified when the user clicks the button. In the callback function, we first show the alert message. After this we use $apply to invoke the pre-compiled method accessor inside of an AngularJS digest cycle. (You can learn about these parts in more depth in Advanced Directives - Combining AngularJS with Existing Components).

If you run this demo, you can see that the alert message is shown before invoking the call to increment().

 

This was a preview-quality chapter of our continuously deployed eBook on AngularJS for .NET developers. If you enjoyed this chapter, you can read more about the project at http://henriquat.re. You can also follow us on twitter (Project: @henriquatreJS, Authors: @ingorammer and @christianweyer)