Introduction to AngularJS
AngularJS is Google's framework for developing Web applications. Angular provides a number of essential services that work very well together and were designed to be extensible. These services include data-binding, DOM manipulation, routing/view management, module loading, and more.
AngularJS is not just another library. It provides a complete integrated framework, so it reduces the number of libraries you have to deal with. It comes from Google, the same people who built Chrome and are helping create the foundations for the next generation of web applications (for more on that check out the polymer project at www.polymer-project.org/). I believe that in five or ten years, we won't be using AngularJS to develop web apps anymore, but we will be using something similar to it.
To me, the most exciting feature of AngularJS is the ability to write custom directives. Custom directives allow you to extend HTML with new tags and attributes. Directives can be reused within and across projects, and are roughly equivalent to custom controls in platforms like .NET.
The sample included with this article includes nearly 50 custom directives created based on Bootstrap, Google JavaScript APIs, and Wijmo. The sample is fully commented and includes documentation, so it should serve as a good reference when you start writing your directives. You can see the sample live here: http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer
Creating directives tailored to your needs is fairly easy. These directives can be tested, maintained, and re-used in multiple projects. Properly implemented directives can be enhanced and re-deployed with little or no change to the applications that use them.
This document focuses on AngularJS directives, but before we get into that topic we will quickly go over some AngularJS basics to provide the context.
To use AngularJS, you must include it as a reference in your HTML page, and add an ng-app
attribute to the HTML or body tags on the page. Here is a very short sample to show how this works:
<html>
<head>
<script src="http://code.angularjs.org/angular-1.0.1.js"></script>
</head>
<body ng-app ng-init="msg = 'hello world'">
<input ng-model="msg" />
<p>{{msg}}</p>
</body>
</html>
When AngularJS loads, it scans the document for the ng-app
attribute. This tag is usually set to the name of the application's main module. Once the ng-app
attribute is found, Angular will process the document, loading the main module and its dependencies, scanning the document for custom directives, and so on.
In this example, the ng-init
attribute initializes an msg
variable to "hello world" and the ng-model
attribute binds the content of the variable to an input element. The text enclosed in curly braces is a binding expression. AngularJS evaluates the expression and updates the document whenever the value of the expression changes. You can see this in action here: jsfiddle.net/Wijmo/HvSQQ/
AngularJS Modules
Module objects serve as the root of AngularJS applications. They contain objects such as config
, controller
, factory
, filter
, directive
, and a few others.
If you are familiar with .NET and new to Angular, the table below shows a rough analogy that helps explain the role played by each type of AngularJS object:
AngularJS | .NET | Comment |
module | Assembly | Application building block |
controller | ViewModel | Contains the application logic and exposes it to views |
scope | DataContext | Provides data that can be bound to view elements |
filter | ValueConverter | Modifies data before it reaches the view |
directive | Component | Re-usable UI element |
factory, service | Utility classes | Provide services to other module elements |
For example, this code creates a module with a controller, a filter, and a directive:
var myApp = angular.module("myApp", []);
myApp.controller("myCtrl", function($scope) {
$scope.msg = "hello world";
});
myApp.filter("myUpperFilter", function() {
return function(input) {
return input.toUpperCase();
}
});
myApp.directive("myDctv", function() {
return function(scope, element, attrs) {
element.bind("mouseenter", function() {
element.css("background", "yellow");
});
element.bind("mouseleave", function() {
element.css("background", "none");
});
}
});
The module
method takes as parameters the module name and a list of dependencies. In this example, we are creating a module that does not depend on any other modules, so the list is empty. Note that the array must be specified, however, even if it is empty. Omitting it would cause AngularJS to retrieve a named module specified previously. We will discuss this in more detail in the next section.
The controller
constructor gets a $scope
object that is responsible for holding all the properties and methods exposed by the controller. This scope will be managed by Angular and passed to views and directives. In this example, the controller adds a single msg
property to the scope. An application module may have multiple controllers, each responsible for one or more views. Controllers do not have to be members of the module, but it is good practice to make them so.
The filter
constructor returns a function that will be used to modify input for display. Angular provides several filters, but you can add your own and use them in exactly the same way. In this example we define a filter that converts strings to uppercase. Filters can be used not only to format values, but also to modify arrays. Formatting filters provided by AngularJS include number
, date
, currency
, uppercase
, and lowercase
. Array filters include filter
, orderBy
, and limitTo
. Filters may take parameters, and the syntax is always the same: someValue | filterName:filterParameter1:filterParameter2....
The directive
constructor returns a function that takes an element and modifies it according to parameters defined in the scope. In this example we bind event handlers to the mouseenter
and mouseleave
events to highlight the element content when the mouse is over it. This is our very first directive, and barely scratches the surface of what directives can do. AngularJS directives can be used as attributes or elements (or even comments), and they can be nested and communicate with each other. We will cover a lot of that in later sections.
Here is a page that uses this module:
<body ng-app="myApp" ng-controller="myCtrl">
<input ng-model="msg" />
<p my-dctv >
{{ msg | myUpperFilter }}
</p>
</body>
You can see this in action here: jsfiddle.net/Wijmo/JKBbV/
Notice that the names of the app module, controller, and filter are used as attribute values
. They represent JavaScript objects and therefore these names are case-sensitive.
The name of the directive, on the other hand, is used as an attribute name
. It represents an HTML element, and therefore is case-insensitive. However, AngularJS converts camel-cased directive names to hyphen-separated strings. So the "myDctv" directive becomes "my-dctv" (just like the built-in directives ngApp
, ngController
, and ngModel
become "ng-app", "ng-controller", and "ng-model".
Project Organization
AngularJS was designed to handle large projects. You can break up your projects into multiple modules, split modules into multiple files, and organize these files in whatever way makes sense to you. Most projects I have seen tend to follow the convention suggested by Brian Ford in his blog Building Huuuuuge Apps with AngularJS. The general idea is to break up modules into files and to group them by type. So controllers are placed in a controllers folder (and named XXXCtrl
), directives go in a directives folder (and are named XXXDctv
), etc.
A typical project folder might look like this:
Root
default.html
styles
app.css
partials
home.html
product.html
store.html
scripts
app.js
controllers
productCtrl.js
storeCtrl.js
directives
gridDctv.js
chartDctv.js
filters
formatFilter.js
services
dataSvc.js
vendor
angular.js
angular.min.js
Imagine for example that you want to use a single module, defined in the app.js file. You could define it this way:
angular.module("appModule", []);
To add elements to the module, you would then ask for the module by name and add elements to it as we showed before. For example, the formatFilter.js file would contain something like this:
var app = angular.module("appModule");
app.filter("formatFilter", function() {
return function(input, format) {
return Globalize.format(input, format);
}
}})
If your app contains multiple modules, remember to specify the dependencies when you create each module. For example, an application that contained three modules named app
, controls
, and data
could specify them as follows:
angular.module("app", [ "controls", "data"])
angular.module("controls", [ "data" ])
angular.module("data", [])
The main page in your application would specify the name of the main module in the ng-app
directive, and AngularJS would automatically bring in all the required dependencies:
<html ng-app="app">
...
</html>
The main page and all its views would then be able to use elements defined in the three modules.
For an example of a fairly large application organized in the manner described above, please see the AngularExplorer sample included with this article.
Now that we have covered the basics of AngularJS, it is time to deal with our main topic: directives. In the next few chapters, we will cover the basic concepts and will create quite a few directives to demonstrate their possibilities, which are quite amazing.
If you want to learn a bit more about AngularJS before continuing (or at any time really), I recommend Dan Wahling's excellent video " AngularJS Fundamentals in 60-ish Minutes". There are also some interesting videos put together by members of the AngularJS team on the " About those directives" page.
AngularJS Directives: Why?
I said earlier that to me directives are the most exciting feature of AngularJS. That's because they are the one feature that is really unique to AngularJS. Great as they are, the other features in AngularJS are also available in other frameworks. But the ability to create reusable libraries of components that can be added to applications in pure HTML is something incredibly powerful, and to my knowledge AngularJS is the only framework that provides that capability to web applications today.
There are several JavaScript products that provide controls to web developers. For example, Boostrap is a popular "front-end framework" that provides styles and some JavaScript components. The problem is that in order to use the components, the HTML author must switch into JavaScript mode and write jQuery code to activate the tabs. The jQuery code is simple enough, but it has to be synchronized with the HTML, and this is a tedious and error-prone process that does not scale well.
The AngularJS home page shows a simple directive that wraps the Bootstrap tab component and makes it really easy to use in pure HTML. The directive makes tabs as easy to use as ordered lists. Plus, the directive can be re-used in many projects by many HTML developers. The HTML is as simple as this:
<body ng-app="components">
<h3>BootStrap Tab Component</h3>
<tabs>
<pane title="First Tab">
<div>This is the content of the first tab.</div>
</pane>
<pane title="Second Tab">
<div>This is the content of the second tab.</div>
</pane>
</tabs>
</body>
You can see this in action here: jsfiddle.net/Wijmo/ywUYQ/
As you can see, the page looks like regular HTML, except it has been extended with <tabs>
and <pane>
tags implemented as directives. The HTML developer doesn't have to write any JavaScript. Of course, someone has to write the directives, but those are generic. They can be written once and reused many times (just like BootStrap, jQueryUI, Wijmo, and all those other great libraries).
Because directives are so useful, and not all that hard to write, many people are already creating directives for popular libraries. For example, the AngularJS team has created a set of directives for Boostrap called UI Bootstrap; ComponentOne ships AngularJS directives with its Wijmo library; and there are several public repositories of directives for jQueryUI widgets.
But wait a minute! If there are so many sources of ready-made directives, why should you learn how to create them yourself? Good question. Maybe you don't. So look around before writing your own. But there are a couple of good reasons to learn:
- You may have special needs. Suppose for example you work for a financial company that uses a certain type of form across many applications. The form can be implemented as a data grid, with custom functionality to download data in a certain way, edit and validate the data in a certain way, and upload the changes back to the server in a certain way. It is unlikely that anyone outside your corporation will have something useful to you. But you could write a custom directive and make it available to all HTML developers on your team that would allow them to write:
<body ng-app="abcFinance">
<h3>Offshore Investment Summary</h3>
<abc-investment-form
customer="currentCustomer"
country="currentCountry">
</abc-investment-form data>
</body>
The "abcInvestmentForm
" directive could be used in many applications, providing consistency. The directive would be centrally maintained and could be updated to reflect new business practices or requirements with little impact on the applications.
- Maybe the directive you want really doesn't exist yet. Perhaps you happen to like a library that nobody wrote directives for yet, and you don't want to wait. Or maybe you simply don't like the directives that you found, and you would like to tweak them.
OK, I guess if you are reading this article you are already sold on the idea of directives and eager to get started. So let's move on.
AngularJS Directives: How?
The directive we showed in the beginning of this article was very simple. It only specified a "link" function and nothing else. A typical directive contains more elements:
var m = angular.module("myApp");
myApp.directive("myDir", function() {
return {
restrict: "E",
scope: {
name: "@",
amount: "=",
save: "&"
},
template:
"<div>" +
" {{name}}: <input ng-model='amount' />" +
" <button ng-click='save()'>Save</button>" +
"</div>",
replace: true,
transclude: false,
controller: [ "$scope", function ($scope) { … }],
link: function (scope, element, attrs, controller) {…}
}
});
Note how the directive name follows a pattern: the "my" prefix is analogous to a namespace, so if the application uses directives from multiple modules it will be easy to determine where they are defined. This is not a requirement, but it is a recommended practice that makes a lot of sense.
The directive constructor returns an object with several properties. These are all documented in the AngularJS site, but the explanations they provide are always as clear as they should be. So here is my attempt at explaining what these properties do:
restrict
: Determines whether the directive will be used in HTML. The valid options are "A", "E", "C", and "M" for attribute
, element
, class
, or comment
. The default is "A", for attribute. But we are more interested in element attributes, because that's how you create UI elements such as the "tab" directive shown earlier.scope
: Creates an isolated scope that belongs to the directive, isolating it from the scope of the caller. Scope variables are passed in as attributes in the directive tag. This isolation is essential when creating reusable components, which should not rely on the parent scope. The scope
object defines the names and types of the scope variables. The example above defines three scope variables:
name: "@" (by value, one-way)
:
The at sign "@" indicates this variable is passed by value. The directive receives a string that contains the value passed in from the parent scope. The directive may use it but it cannot change the value in the parent scope (it is isolated). This is the most common type of variable.amount: "=" (by reference, two-way)
The equals sign "=" indicates this variable is passed by reference. The directive receives a reference to a value in the main scope. The value can be of any type, including complex objects and arrays. The directive may change the value in the parent scope. This type of variable is used when the directive needs to change the value in the parent scope (an editor control for example), when the value is a complex type that cannot be serialized as a string, or when the value is a large array that would be expensive to serialize as a string.save: "&" (expression)
The ampersand "&" indicates this variable holds an expression that is executed in the context of the parent scope. It allows directives to perform actions other than simply changing a value.
template
: String that replaces the element in the original markup. The replacement process migrates all attributes from the old element to the new one. Notice how the template may use variables defined in the isolated scope. This allows you to write macro-style directives that don't require any additional code. In most cases, however, the template is simply an empty <div>
that will be populated using code in the link
function discussed below.replace
: Determines whether the directive template should replace the element in the original markup or be appended to it. The default value is false, which causes the original markup to be preserved.transclude
: Determines whether the custom directive should copy the content in the original markup. For example, the "tab" directive shown earlier had transclude
set to true because the tab element contains other HTML elements. A "dateInput" directive on the other hand would have no HTML content so you would set transclude
to false (or just omit it altogether).link
: This function contains most of the directive logic. It is responsible for performing DOM manipulations, registering event listeners, etc. The link
function takes the following parameters:
scope
: Reference to the directive's isolated scope. The scope
variables are initially undefined, and the link
function registers watches to receive notifications when their values change.element
: Reference to the DOM element that contains the directive. The link
function normally manipulates this element using jQuery (or Angular's jqLite if jQuery is not loaded).controller
: Used in scenarios with nested directives. This parameter provides child directives with a reference to the parent, allowing the directives to communicate. The tab directive discussed earlier is a good example: jsfiddle.net/Wijmo/ywUYQ/
Note that when the link
function is called, the scope variables passed by value ("@") will not have been initialized yet. They will be initialized at a later point in the directive life cycle, and if you want to receive notifications you have to use the scope.$watch
function, discussed in the next section.
If you are not familiar with directives yet, the best way to really understand all this is to play with some code and try out different things. This fiddle lets you do that: jsfiddle.net/Wijmo/LyJ2T/
The fiddle defines a controller with three members (customerName
, credit
, and save
). It also defines a directive similar to the one listed above, with an isolated scope with three members (name
, amount
, and save
). The HTML shows how you can use the controller in plain HTML and with the directive. Try changing the markup, the types of the isolated variables, the template, and so on. This should give you a good idea of how directives work.
Communication between Directive and Parent Scopes
OK, so directives should have their own isolated scope so they can be re-used in different projects and be bound to different parent scopes. But how exactly do these scopes communicate?
For example, assume you have a directive with an isolated scope declared as in the example above:
scope: {
name: "@",
amount: "=",
save: "&"
},
And assume the directive is used in this context:
<my-dir
name="{{customerName}}"
amount="customerCredit"
save="saveCustomer()"
/>
Notice how the "name" attribute is enclosed in curly brackets and "amount" is not. That is because "name" is passed by value. Without the brackets, the value would be set to the string "customerName". The brackets cause AngularJS to evaluate the expression before and set the attribute value to the result. In contrast, "amount" is a reference, so you don't need brackets.
The directive could retrieve the values of the scope variables simply by reading them off the scope
object:
var name = scope.name;
var amount = scope.amount;
This would indeed return the current value of the variables, but if the values changed in the parent scope, the directive would not know about it. To be notified of these changes, it would have to add watchers to these expressions. This can be done with the scope.$watch
method, which is defined as:
scope.$watch(watchExpression, listenerFunction, objectEquality);
The watchExpression
is the thing you want to watch (in our example, "name" and "amount"). The listenerFunction
is the function that gets called when the expressions change value. This function is responsible for updating the directive to reflect the new values.
The last argument, objectEquality
, determines how AngularJS should compare the variable's old and new values. If you set objectEquality
to true, then AngularJS will do a deep comparison between the old and new values rather than a simple reference comparison. This is very important when the scope variable is a reference ("=") rather than a value ("@"). For example if the variable is an array or a complex object, setting objectEquality
to true will cause the listenerFunction
to be called even if the variable is still referencing the same array of object, but the contents of the array or object have changed.
Going back to our example, you could watch for changes in the scope variables using this code:
scope.$watch("name", function(newValue, oldValue, srcScope) {
});
scope.$watch("amount", function(newValue, oldValue, srcScope) {
});
Notice that the listenerFunction
gets passed the new and old values, as well as the scope object itself. You will rarely need these arguments since the new value is already set on the scope, but in some cases you may want to inspect exactly what changed. And in some rare cases, the new and old values might actually be the same. This may happen while the directive is being initialized.
How about the other direction? In our example, the "amount" variable is a reference to a value, and the parent scope may be watching it for changes the same way we are.
In most cases you don't have to do anything at all. AngularJS automatically detects changes that happen as a result of user interactions and processes all the watchers for you. But this is not always the case. Changes that happen because of browser DOM events, setTimeout
, XHR, or third party libraries are not detected by Angular. In these cases, you should call the scope.$apply
method, which will broadcast the change to all registered listeners.
For example, imagine our directive has a method called updateAmount
that performs some calculations and changes the value of the "amount" property. Here's how you would implement that:
function updateAmount() {
scope.amount = scope.amount * 1.12;
if (!scope.$$phase) scope.$apply("amount");
}
The scope.$$phase
variable is set by AngularJS while it is updating the scope variables. We test this variable to avoid calling $apply
from within an update cycle.
Summarizing, scope.$watch
handles inbound change notifications and scope.$apply
handles outbound change notifications (but you rarely have to call it).
As usual, the best way to really understand something is to watch it in action. The fiddle at jsfiddle.net/Wijmo/aX7PY/ defines a controller and a directive. Both have methods that change data in an array, and both listen to changes applied by each other. Try commenting out the calls to scope.$watch
and scope.$apply
to see their effect.
Shared Code / Dependency Injection
When you start writing directives, you will probably create utility methods that are useful to many directives. Of course you don't want to duplicate that code, so it makes sense to group these utilities and expose them to all directives that need them.
You can accomplish this by adding a factory
to the module that contains the directives, and then specifying the factory name in the directive constructors. For example:
var app = angular.module("app", []);
app.factory("myUtil", function () {
return {
watchScope: function (scope, props, updateFn, updateOnTimer) {
var cnt = props.length;
angular.forEach(props, function (prop) {
scope.$watch(prop, function (value) {
if (--cnt <= 0) {
if (updateOnTimer) {
if (scope.updateTimeout) clearTimeout(scope.updateTimeout);
scope.updateTimeout = setTimeout(updateFn, 50);
} else {
updateFn();
}
}
})
})
},
apply: function (scope, prop, value) {
if (scope[prop] != value) {
scope[prop] = value;
if (!scope.$$phase) scope.$apply(prop);
}
}
)
});
The "myUtil" factory listed above contains two utility functions:
watchScope
adds watchers for several scope variables and calls an update function when any of them changes, except during directive initialization. It can optionally use a timeout to avoid calling the update function too often.apply
changes the value of a scope variable and notifies listeners of the change (unless the new value is the same as the previous one).
To use these utility functions from a custom directive, you would write:
app.directive("myDir", ["$rootScope", "myUtil",
function ($rootScope, myUtil) {
return {
restrict: "E",
scope: {
v1: "@", v2: "@", v3: "@", v4: "@", v5: "@", v6: "@"
},
template: "<div/>",
link: function (scope, element, attrs) {
var ctr = 0,
arr = ["v1", "v2", "v3", "v4", "v5", "v6"];
myUtil.watchScope(scope, arr, updateFn);
function updateFn() {
console.log("# updating my-dir " + ++ctr);
}
}
}
}]);
As you can see, we simply added the "myUtil" factory to the directive constructor, making all its methods available to the directive.
You can see this code in action in this fiddle: jsfiddle.net/Wijmo/GJm9M/
Despite the apparent simplicity, there's a lot of interesting things going on that make this work. AngularJS examined the directive, detected the "myUtil" parameter, found the "myUtil" factory by name in the module definition, and injected a reference in the right place. Dependency Injection is a deep topic, and is described in the AngularJS documentation.
The fact that the dependency injection mechanism relies on names creates a problem related to minification. When you minify your code to put into production, variable names change and this can break the dependency injection. To work around this issue, AngularJS allows you to declare module elements using an array syntax that includes the argument names as strings. If you look at the directive definition code above, notice that the declaration contains an array with the parameter names (in this case only "myUtil") followed by the actual constructor. This allows AngularJS to look for the "myUtil" factory by name even if the minification process changes the name of the constructor parameter.
Important note on Minification: If you plan to minify your directives, you must use the array declaration technique on all directives that take parameters, and also on
controller declarations that contain parameters. This fact is not well-documented, and will prevent directives with controller functions from working after minification. The Bootstrap tab directive listed in the
Angular home page for example is not minifiable, but this one is:
jsfiddle.net/Wijmo/ywUYQ/.
In addition to factory
, AngularJS includes three other similar concepts: provider
, service
, and value
. The differences between these are subtle. I have been using factories since I started working with Angular and so far have not needed any of the other flavors.
Examples
Now that we've reviewed all the basics, is time to go over a few examples to show how this all works in practice. The next sections describe a few useful directives that illustrate the main points and should help you get started writing your own.
Bootstrap Accordion Directive
Our first example is a pair of directives that create Boostrap accordions:
Bootstrap Accordion Sample
The Bootstrap site has an example that shows how you can create an accordion using plain HTML:
<div class="accordion" id="accordion2">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse"
data-parent="#accordion2" href="#collapseOne">
Collapsible Group Item #1
</a>
</div>
<div id="collapseOne" class="accordion-body collapse in">
<div class="accordion-inner">
Anim pariatur cliche...
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse"
data-parent="#accordion2" href="#collapseTwo">
Collapsible Group Item #2
</a>
</div>
<div id="collapseTwo" class="accordion-body collapse">
<div class="accordion-inner">
Anim pariatur cliche...
</div>
</div>
</div>
</div>
This works, but it is a lot of markup. And the markup contains references based on hrefs and element ids, which make maintenance non-trivial.
Using custom directives you could get the same result using this HTML:
<btst-accordion>
<btst-pane title="<b>First</b> Pane">
<div>Anim pariatur cliche …
</btst-pane>
<btst-pane title="<b>Second</b> Pane">
<div>Anim pariatur cliche …
</btst-pane>
<btst-pane title="<b>Third</b> Pane">
<div>Anim pariatur cliche …
</btst-pane>
</btst-accordion>
This version is much smaller, easier to read and to maintain.
Let's see how this is done. First, we define a module and the "btstAccordion" directive:
var btst = angular.module("btst", []);
btst.directive("btstAccordion", function () {
return {
restrict: "E",
transclude: true,
replace: true,
scope: {},
template:
"<div class='accordion' ng-transclude></div>",
link: function (scope, element, attrs) {
var id = element.attr("id");
if (!id) {
id = "btst-acc" + scope.$id;
element.attr("id", id);
}
var arr = element.find(".accordion-toggle");
for (var i = 0; i < arr.length; i++) {
$(arr[i]).attr("data-parent", "#" + id);
$(arr[i]).attr("href", "#" + id + "collapse" + i);
}
arr = element.find(".accordion-body");
$(arr[0]).addClass("in");
for (var i = 0; i < arr.length; i++) {
$(arr[i]).attr("id", id + "collapse" + i);
}
},
controller: function () {}
};
});
The directive sets transclude
to true because it has HTML content. The template used the ng-transclude
directive to indicate which of the template elements will receive the transcluded content. In this case the template has only one element, so there is no other option, but that is not always the case.
The interesting part of the code is the link
function. It starts by ensuring the accordion element has an id. If it doesn't, the code creates a unique ID based on the ID of the directive's scope. Once the element has an ID, the function uses jQuery to select the child elements that have the class "accordion-toggle" and sets their "data-parent" and "href" attributes. Finally, the code looks for "accordion-body" elements and sets their "collapse" attribute.
The directive also includes a controller
member that contains an empty function. This is required because the accordion will have child elements which will check that the parent is of the proper type and specifies a controller.
The next step is the definition of the accordion pane directive. This one is very simple, most of the action happens right in the template and there's almost no code:
btst.directive('btstPane', function () {
return {
require: "^btstAccordion",
restrict: "E",
transclude: true,
replace: true,
scope: {
title: "@"
},
template:
"<div class='accordion-group'>" +
" <div class='accordion-heading'>" +
" <a class='accordion-toggle' data-toggle='collapse'>{{title}}</a>" +
" </div>" +
"<div class='accordion-body collapse'>" +
" <div class='accordion-inner' ng-transclude></div>" +
" </div>" +
"</div>",
link: function (scope, element, attrs) {
scope.$watch("title", function () {
var hdr = element.find(".accordion-toggle");
hdr.html(scope.title);
});
}
};
});
The require
member specifies that the "btstPane
" directive must be used within a "btstAccordion
". The transclude
member indicates panes will have HTML content. The scope
has a single "title
" property that will be placed in the pane header.
The template
is fairly complex in this case. It was copied directly from the Boostrap sample page. Notice that we used the ng-transclude
directive to mark the element that will receive the transcluded content.
We could stop here. The "{{title}}" property included in the template is enough to show the title in the proper place. However, this approach would only allow plain text in the pane headers. We used the link
function to replace the plain text with HTML so you can have rich content in the accordion headers.
That's it. We have finished our first pair of useful directives. They are small but illustrate some important points and techniques: how to define nested directives, how to generate unique element IDs, how to manipulate the DOM using jQuery, and how to use the $watch
function to listen to changes in scope variables.
Google Maps Directive
The next example is a directive that creates Google maps:
Google Maps Directive Sample
Before we start working on the directive, remember to add a reference to the Google APIs to the HTML page:
<script type="text/javascript"
src="https://maps.googleapis.com/maps/api/js?sensor=true">
</script>
Next, let's define the directive:
var app = angular.module("app", []);
app.directive("appMap", function () {
return {
restrict: "E",
replace: true,
template: "<div></div>",
scope: {
center: "=",
markers: "=",
width: "@",
height: "@",
zoom: "@",
mapTypeId: "@"
},
The center
property is defined as by reference ("=") so it will support two-way binding. The app can change the center and notify the map (when the user selects a location by clicking a button), and the map can also change it and notify the app (when the user selects a location by scrolling the map).
The markers
property is also defined as by reference because it is an array and serializing it as a string could be time-consuming (but it would also work).
The link
function in this case contains a fair amount of code. It has to:
- initialize the map,
- update the map when scope variables change, and
- listen to map events and update the scope.
Here is how this is done:
link: function (scope, element, attrs) {
var toResize, toCenter;
var map;
var currentMarkers;
var arr = ["width", "height", "markers", "mapTypeId"];
for (var i = 0, cnt = arr.length; i < arr.length; i++) {
scope.$watch(arr[i], function () {
if (--cnt <= 0)
updateControl();
});
}
scope.$watch("zoom", function () {
if (map && scope.zoom)
map.setZoom(scope.zoom * 1);
});
scope.$watch("center", function () {
if (map && scope.center)
map.setCenter(getLocation(scope.center));
});
The function that watches the scope variables is similar to the one we described earlier when we discussed sharing code. It calls an updateControl
function when there are any changes to the variables. The updateControl
function actually creates the map using the currently selected options.
The "zoom" and "center" scope variables are treated differently, because we don't want to re-create the map every time the user selects a new location or zooms in or out. These two functions check if the map has been created and simply update it.
Here is the implementation of the updateControl
function:
function updateControl() {
var options = {
center: new google.maps.LatLng(40, -73),
zoom: 6,
mapTypeId: "roadmap"
};
if (scope.center) options.center = getLocation(scope.center);
if (scope.zoom) options.zoom = scope.zoom * 1;
if (scope.mapTypeId) options.mapTypeId = scope.mapTypeId;
map = new google.maps.Map(element[0], options);
updateMarkers();
google.maps.event.addListener(map, 'center_changed', function () {
if (toCenter) clearTimeout(toCenter);
toCenter = setTimeout(function () {
if (scope.center) {
if (map.center.lat() != scope.center.lat ||
map.center.lng() != scope.center.lon) {
scope.center = { lat: map.center.lat(), lon: map.center.lng() };
if (!scope.$$phase) scope.$apply("center");
}
}
}, 500);
}
The updateControl
function starts by preparing an options
object that reflects the scope settings, then uses the options
object to create and initialize the map. This is a common pattern when creating directives that wrap JavaScript widgets.
After creating the map, the function updates the markers and adds an event handler so it is notified when the map center changes. The event handler checks to see if the current map center is different from the scope's center property. If it is, then the handler updates the scope and calls the $apply
function so AngularJS will notify any listeners that the property has changed. This is how two-way binding works in AngularJS.
The updateMarkers
function is pretty simple and does not contain anything that is directly related to AngularJS, so we won't list it here.
In addition to the map directive, this example contains:
- Two filters that convert coordinates expressed as regular numbers into geographic locations such as 33°38'24"N, 85°49'2"W.
- A geo-coder that converts addresses into geographic locations (also based on the Google APIs).
- A method that uses the HTML5 geolocation service to get the user's current location.
Google's mapping APIs are extremely rich. This directive barely scratches the surface of what you can do with it, but hopefully it is enough to get you started if you are interested in developing location-aware applications.
You can find documentation for Google's mapping APIs here: https://developers.google.com/maps/documentation/
You can find Google's licensing terms here: https://developers.google.com/maps/licensing
Wijmo Chart Directive
The next example is a chart that shows experimental data and a linear regression. This sample illustrates the scenario described earlier where you have a particular need that is specialized and unlikely to be covered by standard directives shipped with commercial products:
Wijmo Chart Directive Sample
This chart directive is based on the Wijmo line chart widget, and is used like this:
<app-chart
data="data" x="x" y="y"
reg-parms="reg"
color="blue" >
</app-chart>
The parameters are as follows:
data
: a list of objects with properties to plotx, y
: the names of the properties that will be shown on the x and y axisreg
: the linear regression results, an object with properties that represent the regression parameters and the coefficient of determination (AKA R2).color
: the color of the symbols on the chart.
In the initial version of the directive, the regression was calculated within the chart itself, and the "reg" parameter was not needed. But I decided that was not the right design, because the regression parameters are important outside the chart and should therefore be calculated in the scope of the controller.
Without further ado, here is the directive implementation:
app.directive("appChart", function (appUtil) {
return {
restrict: "E",
replace: true,
scope: {
data: "=",
x: "@",
y: "@",
regParms: "=",
color: "@"
},
template:
"<div></div>",
link: function (scope, element, attrs) {
appUtil.watchScope(scope, ["x", "y", "color"], updateChartControl, true, true);
scope.$watch("data", updateChartData);
This first block of code defines the directive type and scope as usual. The link
function uses the watchScope
method that we presented earlier to watch several scope variables and call an updateChartControl
method whenever any of the scope variables change.
Notice that we use a separate call to the scope.$watch
data because we expect the chart data to change more often than the other properties, so we will provide a more efficient hander called updateChartData
to handle those changes.
Here is the implementation of the updateChartControl
method, which actually creates the chart.
function updateChartControl(prop, val) {
var fontFamily = element.css("fontFamily");
var fontSize = element.css("fontSize");
var textStyle = { "font-family": fontFamily, "font-size": fontSize };
var color = scope.color ? scope.color : "red";
var options = {
seriesStyles: [
{ stroke: color, "stroke-width": 0 },
{ stroke: "black", "stroke-width": 1, "stroke-opacity": .5 }
],
seriesHoverStyles: [
{ stroke: color, "stroke-width": 0 },
{ stroke: "black", "stroke-width": 2, "stroke-opacity": 1 }
],
legend: { visible: false },
showChartLabels: false,
animation: { enabled: false },
seriesTransition: { enabled: false },
axis: {
x: { labels: { style: textStyle }, annoFormatString: "n0" },
y: { labels: { style: textStyle }, annoFormatString: "n0" }
},
textStyle: textStyle
};
element.wijlinechart(options);
updateChartData();
}
The code is similar to what we used in the Google maps directive earlier. It builds an options
object containing configuration information, some of which is based on the directive parameters, and then uses this options
object to create the actual chart by calling the element.wijlinechart
method.
After creating the chart widget, the code calls the updateChartData
method to populate the chart. The updateChartData
method creates two data series. The first represents the data passed in through the scope variables, and the second represents the regression. The first series has as many data points as were passed in by the controller, and is shown as symbols. The second series represents the linear regressions, and therefore has only two points. It is shown as a solid line.
Wijmo Grid Directive
Our last example is a directive that implements an editable data grid:
Wijmo Grid Directive Sample
This directive is based on the Wijmo grid widget, and is used like this:
<wij-grid
data="data"
allow-editing="true"
after-cell-edit="cellEdited(e, args)" >
<wij-grid-column
binding="country" width="100" group="true">
</wij-grid-column>
<wij-grid-column
binding="product" width="140" >
</wij-grid-column>
<wij-grid-column
binding="amount" width="100" format="c2" aggregate="sum" >
</wij-grid-column>
</wij-grid>
The "wij-grid" directive specifies the attributes for the grid, and the "wij-grid-column" directive specifies the attributes for individual grid columns. The markup above defines an editable grid with three columns "country", "product", and "amount". Values are grouped by country and group rows show the total amounts for each group.
The most interest part of this directive is the connection between the parent directive "wij-grid" and this child directives "wij-grid-column". To enable this connection, the parent directive specifies a controller
function as follows:
app.directive("wijGrid", [ "$rootScope", "wijUtil", function ($rootScope, wijUtil) {
return {
restrict: "E",
replace: true,
transclude: true,
template: "<table ng-transclude/>",
scope: {
data: "=",
allowEditing: "@",
afterCellEdit: "&",
allowWrapping: "@",
frozenColumns: "@"
},
controller: ["$scope", function ($scope) {
$scope.columns = [];
this.addColumn = function (column) {
$scope.columns.push(column);
}
}],
link: function (scope, element, attrs) {
}
}
}]);
The controller
function is declared using the array syntax mentioned earlier so it can be minified. In this example, the controller defines an addColumn
function that will be called by the child "wij-grid-column" directives. The parent directive will then have access to the column information specified in the markup.
Here is how the "wij-grid-column" directive uses this function:
app.directive("wijGridColumn", function () {
return {
require: "^wijGrid",
restrict: "E",
replace: true,
template: "<div></div>",
scope: {
binding: "@",
header: "@",
format: "@",
width: "@",
aggregate: "@",
group: "@",
groupHeader: "@"
},
link: function (scope, element, attrs, wijGrid) {
wijGrid.addColumn(scope);
}
}
});
The require
member specifies that the "wij-grid-column" directive requires a parent directive of type "wij-grid". The link function receives a reference to the parent directive (controller) and uses the addColumn
method to pass its own scope to the parent. The scope contains all the information needed by the grid to create the column.
More Directives
In addition to the examples discussed in this article, the sample attached contains almost 50 other directives that you can use and modify. The sample application itself is structured following the principles suggested here, so you should have no problems navigating it.
In the sample, the directives can be found in three files under the scripts/directives folder:
- btstDctv: Contains 13 directives based on the Bootstrap library. The directives include tabs, accordion, popover, tooltip, menu, typeahead, and numericInput.
- googleDctv: Contains two directives based on Google's JavaScript APIs: a map and a chart.
- wijDctv: Contains 24 directives based on the Wijmo library. The directives include input, layout, grids, and charts.
All three directive modules are included in source and minified format. We used Google's Closure minifier, which you can use on-line here: http://closure-compiler.appspot.com/home.
There is an on-line version of the Angular Explorer sample here: http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer.
Conclusion
I hope you had fun reading this article and that you are as excited about AngularJS and custom directives as I am.
Please feel free to use the code in the sample and to contact me with any feedback you may have. I am especially interested in ideas for new directives and on ways to make the directives presented more powerful and useful.
References
- AngularJS by Google. The AngularJS home page.
- AngularJS Directives documentation. The official documentation on AngularJS directives.
- AngularJS directives and the computer science of JavaScript. Interesting article on writing AngularJS directives.
- Video Tutorial: AngularJS Fundamentals in 60-ish Minutes. A nice video introducing AngularJS by Dan Wahling.
- About those directives. A series of videos on directives and more by members of the AngularJS team.
- Egghead.io. A series of how-to videos on AngularJS by John Lindquist.
- Polymer Project. What is coming after AngularJS.
- Wijmo AngularJS Samples. Several on-line demos created using AngularJS and custom directives.