AngularJS Directives – building a DSL with your HTML

by
Tags: , , , , ,
Category:

I will admit in advance – this content trades off of a great little screencast by Brian Ford from CODEShow on AngularJS directives. Go watch that if you have a little knowledge of AngularJS and 45 minutes to kill… But if not…

What is AngularJS?

Google built AngularJS, a Javascript MVC (well, MV VM, more later) to write single-page web applications. It provides an application framework, which lets you define view templates for your HTML content, register MVC controllers (for handling activities, listening for events and initializing views), build services (stateful or stateless Javascript code for handling integration to outside data sources and sinks), and several other components, including directives.

As I mentioned before, we’ll get to directives in a bit. But first, the 5 minute AngularJS sample.

AngularJS in a Nutshell

AngularJS provides a bridge between your HTML and Javascript via a nice two-way data-binding mechanism. Angular uses a view model object, which can be set within your controllers, to send data to a page. A one-way example:

ARVE Error: need id and provider

To play with this, just click on the various headings (Javascript, HTML, Result). I’m using JSFiddle, an incredibly useful playground where you can quickly cook up snippets of whatever you wish and share them online, even associate them with your GitHub account.

Breakdown – the Module

You can see that we’ve defined an module named ‘demo’ (ignore those square brackets for now):

var demo = angular.module('demo', []);

This module defines an application for us. We can eventually register a number of elements in this application, but for now, we’ll just set up the application itself so it can be bound to a portion of our page.

The Controller and ‘ViewModel’

We then defined a controller called ‘MyCtrl’:

function MyCtrl($scope) {
...
}

which will automatically inject the magically named ‘scope’ variable – $scope. Just like Spring or other dependency injection systems, provided you know the service you’re injecting, you specify it by name. Built-in and add-on services from Angular generally use the ‘$’ prefix. Yours will not. That’s a way of distinguishing user code from system-built code.

The ‘$scope’ variable is a ViewModel – meaning a model specific to display of a template or view. To demystify the whole ‘MVVM’ term you may have heard, consider the ViewModel is simply the model that is given to the view, so that you can decouple it from your application’s internal model, some of which you may not want to share with the page at a given time. Those darn terms…

The View

Now we come to the view. In our case, we’ve decided to make this view static – meaning that it’s embedded into the page. We can certainly set up external views in another post, but for now, let’s just focus on the content. You’ll notice in the HTML we’ve defined two special tag attributes that you won’t recognize, ‘ng-app’ and ‘ng-controller’.

Greeting

Hi, it's Ken. The current time is {{time}}.

The ng-controller attribute is easy – it tells the page to watch for any data exported from our controller, named MyCtrl, and inject data stored in the $scope variable by its name. You’ll notice above we left out our controller implementation. Let’s look at it now:

function MyCtrl ($scope) {
    $scope.time = new Date().toTimeString();
}

Now, you can see the linkage – the $scope.time variable becomes {{time}} in the page. But what about the ng-app attribute?

The ng-app attribute defines what the scope of a given application might be on a given page. Like the controller, this limits the exposure of the application to a given area of the page. An application may be composed of elements such as controllers, services, views, directives, etc., and you may have need of Angular on several completely unrelated places on a given page. That’s OK, you can create two applications, or an outer application and two related inner applications, all by defining modules. Further, once you start building components you’ll likely want to set up more than one controller on a given view page, rather than one monolithic controller and view.

A Sample Directive

Ok, so let’s bring it home. Maybe we want to think of the time as a ‘feature’ – something we’d use on the page in a few places. What if we wanted it to be referred to by a ‘display-current-time’ HTML tag? Something like this:

Hi, it's Ken. The current time is

To define a tag like that, you use a directive – a Javascript component that provides an HTML tag, attribute or CSS class with YOUR defined name, that then behaves like a DSL language element in the page.

Defining a Directive

Let’s begin with what we want our HTML to look like. This is a completely trivial and silly example, but it gives you an idea of what you can do.

Hi, it's {{name}}. The current time is {{time}}.

I didn’t want to remove the controller, so I used it to bind the name to ‘Ken’. Here’s the controller now:

function MyCtrl ($scope) {
    $scope.name = 'Ken';
}

Ok, our manager is going to freak out. Maybe later we’ll externalize this into a form field, and show you that nifty two-way binding.

The Directive

Now for the directive. We’ll add it to the javascript sample file, and since we’ve defined our application (demo) above, we simply add the directive to it.

demo.directive('displayCurrentTime', function() {
    return {
        restrict: 'E',
        template: '12:00:03'
    };
});

The directive is given a camel-cased name – displayCurrentTime and in the inimitable Javascript way, we provide the implementation as an inlined anonymous function. The directive must return at least a template to render, and optionally a narrowed-down ‘restrict’ variable which tells Angular to activate the directive for HTML tags/elements (E), HTML tag attributes (A), or CSS classes ‘C’. In the example above, we’re going to only activate it for tags.

Angular expects that camel-cased name to turn into a lower-cased, dash-separated name in the HTML. Hence why our tags above are called ‘display-current-time’.

Getting Dynamic

Now let’s get a bit more code-driven and dynamically generate the time.

demo.directive('displayTime', function($parse) {
    return {
        restrict: 'E',
        replace: true,
        transclude: false,
        template: '',
        link: function (scope, element, attrs, controller) {
            var currentDate = new Date();
            element.text(currentDate.toTimeString());
        }
    }});

One key function of a directive is to link the content in the model to DOM elements on the page. We supply a link function definition, and AngularJS injects the element as the second parameter. We then create a Javascript date with an empty constructor (current time) and use the element’s text method to send it to the content of the span itself. In more advanced cases, we can supply a compile method, which is more powerful, but for simple cases like this the link method will suffice.

Play with this snippet:

ARVE Error: need id and provider

By the way, if you begin trying to troubleshoot issues with your AngularJS or other framework app, some developers insist you reproduce the core problem in a tool like JSFiddle. It’s a good way to play with a sandbox with zero setup.

Marco… Polo!

Let’s take this one step further, and implement another directive to provide a business-level component. Perhaps you need to provide a list of user roles in a drop-down element, and you want to notify the rest of the application when you’ve changed the current one. In AngularJS, a typical way to do this is via a controller and view:

ARVE Error: need id and provider

Play around with the values, and open Firebug or your Chrome developer tools (sorry IE users, I’m sure there’s a way but I’ve been a mac user for too long). You’ll see the console writing the change as we make it. Note the name of the attributes all begin with ng-. That’s short for Angular. These are built-in attributes of the various supported widgets. Read up on the various ngXXX API attributes/elements to learn how to control text boxes, buttons, text areas, and other objects.

The DSL for Groups

Let’s say you want to set up a widget for group selection, with its own styling and form elements. You might start by coding it directly using a controller/view combination. First, we’re using a simple form, with some AngularJS decoration here. From the HTML:

 

Now, we’ll bring in the AngularJS controller. We’ll break this down step-by-step. First, we provide the application and a controller:

var demo = angular.module('demo', []);
function MyCtrl ($scope, $rootScope) {
    $scope.myGroups = [
        {label:'Admin', value:1},
        {label:'Users', value:2},
        {label:'Public', value:3}
    ];
    ...
}

The group list is arbitrary here – we could easily use Angular’s $http component or even a RESTful resource to fetch the values. Note that the myGroups scope variable lines up with the curious expression in the ng-options attribute of our select tag. Let’s explain that:

o.value as o.label for o in myGroups

What? Ok, this is almost like a select statement. It’s one of the things in Angular you have to read through a few times to get it right. This expression assumes we are passing it an array of JS objects (myGroups) and that we’re providing both a display and an option for the option tag it generates. Reading the fragments from the right, we first assign an alias ‘o’ to myGroups (o in myGroups). Then, we provide an optional label in the middle (o.label), and the data value (o.value). Hence, ‘o.value as o.label for o in myGroups’. Easy, peasy? Well no, but it does work. See the select directive and the cries for better documentation in the comments to make sure you’re not the only crazy one.

Handling the Change Event

Rather than immediately changing the group for our application, we want to do it once the user clicks the button. So in that way, our currentGroup model element is transient. To do that, we’ll define a function that takes the immediately bound model element currentGroup and broadcasts it to the rest of the application:

$scope.applyNewGroup = function (currentGroup) {
    $rootScope.$broadcast("new group", currentGroup);
};
// this could be anywhere in our application...
$scope.$on("new group", function(obj, value) {
    console.log("Global group switched to " + JSON.stringify(value));
});

You may not have noticed but in our controller, we not only requested $scope, but $rootScope. The root scope is a scope that all controllers in our application can see. If we wanted our controllers to be notified when the user changes groups, we simply call the root scope’s $broadcast method, passing it a message and optionally, data to send. We’ve done that by passing the group id along. To subscribe to a message, in any sub-scope of the application (any controller), we just use the local scope’s $on method. The scope is passed the object generating the event and the value passed. Since the value is our group, we accept it.

Put It All Together!

Now we’ll put in the magic sauce. Let’s externalize the form into a template string (yes, this can be an URL instead and loaded as a view file) and use the directive function to set it up:

demo.directive('userGroupSwitcher', function() {
  return {
    restrict: 'E',
    replace: true,
    transclude: false,
    template: '
    
' }; });

Then, to place the tag on the screen, we use:

    <user-group-switcher />

Play around with it here:

ARVE Error: need id and provider

Summary

AngularJS is a powerful client-side Javascript application platform. You can use as little or as much of it as you wish. There are tons of samples online about building Angular web applications with simple controllers and forms, but I wanted to show you how to write directives – a way of liberating your domain-specific features from DOM elements, and allowing you to assemble views using business-specific building blocks. You can use directives for anything from menus, to re-use of forms, to wrapping other JS component libraries and widgets, to anything you can think of. Combining directives with Angular’s message passing, easy REST/Ajax support, two-way data binding, support for templates and dependency injection, it’s hard to pick a better framework (in my opinion) that can get you started quicker or provide you more flexibility without forcing you to build your own skeleton.

Resources

For finding more AngularJS component libraries based on directives, see ngmodules.org. A few of my favorites: Angular Bootstrap, for wrapping / encapsulating Twitter bootstrap features using directive tags and attributes, Angular UI, a set of widgets based on jQuery UI, and one I’m hacking with right now, jQuery Mobile Angular Adapter which tries to settle the score between Angular’s dynamic views and jQuery Mobile’s jump-link based dialog switching. For hacking around with Angular, please DO use JS Fiddle and search around for some good ones to start from. Feel free to fork any of mine to get going. In my next article, I’ll show you more of the inner workings of Angular directives. If you want to learn that beforehand, go to the above-mentioned projects’ GitHub pages and start reading. Marco!