ASP.NET MVC6 Angular Tutorial

Basic ASP.NET 5 MVC6 Angular tutorial with easy gulp tooling.

View on GitHub

Part 4: Creating a custom directive

This continues on from the work done in part 3, if you haven't completed that, it's best to head over there. Alternatively, you can switch to the branch "Part03Final" (Team Explorer - Branches - right click "Part03Final" inside remotes/origin and select "New local branch from..." and give it a local name, tick Checkout branch and untick Track remote branch) to use our version of part 3.

Creating a directive to show a list of things

We've decided that we might need to show lists of things all over the place in our application, and the easiest way of doing that is to create a custom directive. Let's dive right in - create a folder named directives, and in it, create two new files - a javascript file named agsThingList.directive.js, and an HTML file named agsThingList.html.

Directive Naming

Why have we named this agsThingList? For a directive, when you use it in your templates, you will call it as an element or attribute which will have a name derived from your name, for example agsThingList would be referenced as:

<ags-thing-list></ags-thing-list>

... or ...

<div ags-thing-list=""></div>

Note the camelCase is replaced with hyphens. But why ags? There is a single common namespace for directives, so for this tutorial, I've picked ags (AngularGettingStarted) as a prefix for the local directives we're creating. This makes sure we don't clash with any pre-existing directives (for example, the in-built Angular directives all start ng-). You can choose any prefix you like, or none at all, but for clean code it makes a lot of sense to choose one and use it for all your directives in a particular project.

Implementing the directive

In your agsThingList.directive.js, pop in this boilerplate, then we'll go through what the various parts mean:

(function () {
    'use strict';
    // Registers this directive with the Angular module "app"
    angular.module('app').directive('agsThingList', agsThingList);

    function agsThingList() {
        var directive = {
            // The link function is where we do any manipulation of the
            // HTML:
            link: link,
            // Define the template:
            templateUrl: "js/src/directives/agsThingList.html",
            // Define the inputs/outputs of this directive:
            scope: {
            },
            // Restrict where this directive can be used - E=element, 
            // A=attribute, EA=either.
            restrict: "EA",
            // Define a controller for this directive.
            controller: agsThingListController,
            // Define the alias for the controller for use in the 
            // template
            controllerAs: "agsThingList",
            // Ensure our input/output scope above is tied to our 
            // controller below.
            bindToController: true
        }
        
        return directive;

        function link(scope, el, attr, ctrl) {
        }
    }

    // Inject any services and data this controller depends on.
    agsThingListController.$inject = [];

    function agsThingListController() {
        var vm = this;
        // "Public" properties

        // "Public" functions

        // "Private" properties

        // Initialisation

        // "Public" function definitions

        // "Private" function definitions

        // Event Subscriptions
    }
})();

The important part of defining the directive comes at the top of this, so let's quickly talk through the key parts of that definition:

link
The link function is run when the directive has data bound to it when it is created. It is in here that any DOM manipulation (i.e. adding/altering the HTML) should be done.
scope
This defines what the inputs and outputs of this directive are. If used as an element, any attributes added to the element are available here, and can be bound in different ways. We'll use some examples of this later in this section.
restrict
This controls how a directive can be included by other templates - E means it should use the "element" syntax (e.g. <ags-thing-list></ags-thing-list>) and A means it should use the "attribute" syntax (e.g. <div ags-thing-list=""></div>). EA means either is fine.
controller
Much like the regular page we created previously, a directive has a controller too. Whether code should go in the link or controller can be somewhat confusing for a directives, it can often go in either. Remember that the link should be doing any DOM manipulation, and the controller should be dealing with the "external" API of the directive. There's many shades of grey here...
controllerAs
With our regular page, we defined the controller alias in-line in a single "Yyy as yyy" line, on a directive it is separated into two, but has the same meaning - this is the alias we'll use in our template to refer to our controller.
bindToController
This tells Angular that we want our inputs/outputs (as defined by the scope argument) to be bound to the controller we've specified (e.g. if we have a scope property named "title", this will be bound to vm.title inside our controller).

After the definition, we have our link and controller functions - note that we've got the normal $inject syntax for adding in any dependencies to our controller - but also remember that in a directive, we shouldn't really be loading any data from servers or similar, data should be provided to our directive by the calling template/controller pair. So, for example, you're unlikely to be adding a dependency on our thingListService here.

Adding in some scope

We're trying to make a directive to show our list of things, so let's update our directive to have a scope property for the list of things to show. In the agsThingList.directive.js, update the "scope" property:

scope: {
  list: "=things"
}

This means that take the attribute "things" from the calling template, and put it into our local scope as "list". This creates a property "vm.list" on the directive controller, but do not be tempted to add one to the controller yourself! If you add one yourself, it might work initially, but you get odd problems if the data is updated outside the directive. We'll then call our directive like so:

<ags-thing-list things="home.list"></ags-thing-list>

There's a shortcut for defining the scope if you want the local name to be the same as the attribute name - list: "=" would mean the attribute and controller property would both be called list.

Now, let's add our template definition in - replace any HTML in agsThingList.html with something like this - note we're using the controllerAs alias to reference the data we're interested in:

<ul>
  <li ng-repeat="item in agsThingList.list">{{item}}</li>
</ul>

This basic directive should be ready now, so in our home.html template, replace our existing list with <ags-thing-list things="home.list"></ags-thing-list> and run it up. Add a couple of items, and you should see them appearing!

Removing items from our list

With our previous version, we could remove items from our list. How should that be implemented in a directive? We need to expose a callback attribute on our scope, which uses the & prefix:

scope: {
  list: "=things",
  removeItem: "&onRemoveItem"
}

Update our home.html template to bind a function call to this as follows, noting how the attribute name is converted from onRemoveItem to on-remove-item:

<ags-thing-list things="home.list" 
            on-remove-item="home.removeItem(item)"></ags-thing-list>

And we need to update our directive template to call this new removeItem handler when a remove link is clicked:

<ul>
  <li ng-repeat="item in agsThingList.list">
    {{item}} 
    (<a href="#" ng-click="agsThingList.removeItem({item: item})">
      remove
    </a>)
  </li>
</ul>

Note the syntax for specifying the parameters to the callback - we're actually passing an object into the callback {item: item} which is translated to the named parameter "item".

Run this up, and it should all be working. So far, this has been more work than doing the list locally in the page template, however if we had multiple places where we were showing lists with remove functionality, we could now re-use this directive in each of those places. And because we've not strongly bound our directive to a particular set of operations against the server, we could use this list anywhere we have an array of items, and bind it to any removal functionality which is appropriate.

It's easy enough to pull functionality out into directives later, so it's OK to err on the side of leaving things in local templates until you know you're going to want to re-use them - with the caveat that directives are the only place you should do any HTML / DOM manipulation, so if you need that, directives are the way to go.

Exercise: Add in a "removing" parameter to the directive

In our previous version, we used a "removing" property on the controller to disable the remove links while we removed an item. Try to add such a property to the new directive and pass its value through from our home controller, and use it to hide the remove links as per our previous version.

Tip: If you're struggling to see if it's working because the remove is happening too quickly, you can hack a "Thread.Sleep(1000)" into your WebAPI delete handler to make it take a bit longer to remove the item.

You can check your solution against the branch Part04Final which shows a final working solution for all the steps discussed in this part of the tutorial.

Once you're done here, you can continue to Part 5 where we look in greater detail at the router - creating multiple pages and ensuring we load the data cleanly for those pages.