Table of contents

< Previous chapterNext chapter >

Last updated .

Using and creating directives #1

The directive is probably the most characteristic construct in Angular: A directive is a custom html element or attribute that extends the markup with behaviour. And it does so in an expressive way. For example, it is pretty obvious what a directive such as ngShow does. When Angular has been set in motion through the use of the ngApp directive on an element, it "compiles" the whole html tree under that element, meaning that it traverses the DOM tree and processes each directive. A directive is thus a hook into Angular, instructing it to construct objects such as controllers.

Angular comes with an array of general purpose directives for various purposes such as manipulating the DOM and setting event handlers. But chances are you may want to develop your own directives to encapsulate common patterns or reusable components. The previous chapter about filters was quite easy, but prepare yourself for a more complex experience with directives!

A directive name will have a number of aliases. The camelCased version is its "real" name and is the version used in script, whereas in markup, the dash-delimited version is used. For example, ngShow is the same directive as ng-show. In addition, in markup one can use several aliases. For example, these five examples of a ngShow directive are valid and equivalent:

<div ng-show="true">Div 1</div> <div ng-SHOW="true">Div 2</div> <div ng:show="true">Div 3</div> <div ng_show="true">Div 4</div> <div data-ng-show="true">Div 5</div>

The purpose of having the variation that starts with "data-" is to enable HTML5 compliance; the html5 standard requires that custom attribute names start with "data-". The recommendation is to use the lowercase dash-delimited name with or without a "data-" prefix.

I stated above that a directive is an attribute on an html element. Actually, it can also be the element name itself, for example the custom myDirective directive could occur in markup as <my-directive></my-directive>. In fact, it may also go in a html comment or inside a class attribute, but that is not recommended.

Creating custom directives

The template property

A directive can be created and registered with the module.directive function. Angular will then pick it up and match it with any html element using it. Let's try and make a really simple nobelLaureate directive to output some text in an element:

<!doctype html> <html> <head> <title>Directives</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body> <div ng-app="app" ng-Controller="myController as vm"> <div nobel-laureate></div> </div> <script src="App/App.js"></script> </body> </html>

Now, the script, which creates the root module, a controller and registers the directive:

App/App.js ( function () { var app = angular.module("app", []); function NobelLaureateDirectiveFunc() { return { template: "Name: {{vm.Laureate.Name}}, born: {{vm.Laureate.Birthday}}" }; } app.directive("nobelLaureate", [NobelLaureateDirectiveFunc]); function MyController() { var vm = this; vm.Laureate = { Name: "Wilhelm Röntgen", Birthday: new Date(1845, 2, 27), }; } app.controller("myController", [MyController]); } )();

As you can see, the directive function used to register a directive, takes the directive name as its first argument and then another argument which is a factory function that returns an object. As for factory functions in general, the factory function is invoked only once when the compiler matches the directive for the first time. In this case, the returned object has just a single template property. That object is what the Angular docs refer to as a Directive Definition Object or DDO. When this sample is run, the directive pulls the value of the template and inserts it in its html element. Notice that the directive name is dash-delimited in the markup, while camelCased in code.

This example illustrates a typical motivation for making a custom directive: if

In the above example I assigned a string to the template property, but it may also be a function that returns a string - please refer to an explanation of that under the templateUrl property below.

The templateUrl property

Instead of hardcoding the template in the directive, its contents can be pulled from a server by using a templateUrl instead of a template property. To investigate this I created this one-liner html file:

App/Laureate.html Name: {{vm.Laureate.Name}}, born: {{vm.Laureate.Birthday}}

I modified the directive to get that file:

App/App.js (partial) function NobelLaureateDirectiveFunc() { return { templateUrl: "app/laureate.html" }; }

With this, Angular will download the html file and insert its contents whereever the directive is found. The path must be relative to the path of the page.

The templateUrl property as a function

This can be made even more dynamic by changing the type of the templateUrl property from a string to a function that returns the url. That way, the decision can be made client-side as to which server resource to request. In the next example, I would like to get the markup for two lists from the server, based on the value of the id attribute on the containing element:

<!doctype html> <html> <head> <title>Directives</title> <script src="Scripts/jquery-2.1.4.js"></script> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body> <div ng-app="app" ng-Controller="myController as vm"> <h4>Male laureates:</h4> <ul id="male" nobel-laureate></ul> <h4>Female laureates:</h4> <ul id="female" nobel-laureate></ul> </div> <script src="App/App.js"></script> </body> </html>

The data for the lists are in two html files:

App/Laureates_female.html <li>Name: Marie Curie, born: 1867-09-07</li> App/Laureates_male.html <li>Name: Wilhelm Röntgen, born: 1845-03-26</li> <li>Name: Niels Bohr, born: 1885-09-07</li>

I also modified the controller and the directive to get the markup for a list, based on the id:

App/App.js
( function () { var app = angular.module("app", []); function NobelLaureateDirectiveFunc() { return { templateUrl: function (elements, attributes) { return "app/laureates_" + attributes.id + ".html"; } }; } app.directive("nobelLaureate", [NobelLaureateDirectiveFunc]); function MyController() { var vm = this; } app.controller("myController", [MyController]); } )();
The list of nobel laureates
The list of nobel laureates

Now, which file to request is dependent upon the value of the id attribute.

The function assigned to the templateUrl property takes two arguments:

Before moving on, I should mention that Angular uses jQuery internally. Angular comes with its own lightweight version of jQuery, called jqLite, containing a subset of the full jQuery library. But if your page already uses jQuery, Angular will switch to using that version instead. It only requires that you reference the jQuery script file before Angular. By the way, if you wish to learn about jQuery, there is no better place than here.

New stuff

module.directive: Registers a directive (factory) with the module. A directive factory returns a Directive Definition Object (DDO) with options for the directive.

DDO.template: Set this property to make the directive inject html (which may include Angular constructs).

DDO.templateUrl: Set this property to make the directive inject html (which may include Angular constructs), pulled from a server.

The restrict property

So far, we have made use of the template and templateUrl properties of a directive. You can also give it a restrict property to restrict which kinds of constructs it can be placed on. As mentioned above, the options are element (E), attribute (A), class (C) and comment (M). The restrict property is a string set to a concatenation of the symbols, for example "MC" to allow the directive only in comments and class attributes. The default is "A", to accept the directive only as an attribute. Changing the above example to allow the directive on elements as well as attributes would involve this:

App/App.js (partial) function NobelLaureateDirectiveFunc() { return { restrict: "AE", templateUrl: function (elements, attributes) { return "app/laureates_" + attributes.id + ".html"; } }; } app.directive("nobelLaureate", [NobelLaureateDirectiveFunc]);

New stuff

DDO.restrict: Used to restrict which types of node a directive can be added to.

Scope

Every Angular directive on the page is associated with a scope. You can think of the scope as a property bag à la the ASP.Net MVC property of the same name, although it's a bit more complicated than that. We have been accessing scope all along in the previous chapters, but implicitly so, because we have been using the recommended controllerAs syntax. When an element on the page is databound, it is the scope it is bound to.

Controller syntax

Before moving on to investigate the concept of scope in relation to custom directives, I will have to revert for a while to the other, not-so-recommended way of declaring a controller, using the $scope explicitly. To illustrate the actual equivalence of the two approaches, I have compiled an example that uses both ways of declaring a controller. A controller is associated with a scope. First, the markup with two controllers, using different declaration forms:

ControllerSyntax.html <!doctype html> <html> <head> <title>ControllerSyntax</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body ng-app="app"> <div ng-Controller="controller"> {{ vm.Name }} </div> <div ng-Controller="controllerAs as vm"> {{ vm.Name }} </div> <script src="ControllerSyntax.js"></script> </body> </html>

And the script:

ControllerSyntax.js
( function () { var app = angular.module('app', []); function Controller(scope) { scope.vm = { Name: "My name is controller" }; } app.controller("controller", ["$scope", Controller]); function ControllerAs() { var vm = this; vm.Name = "My name is controllerAs"; } app.controller("controllerAs", [ControllerAs]); } )();

Notice in the markup that the expressions in markup - {{ vm.Name }} - are identical for the two controllers. Only, the code to set that value in the scope is different in the two cases. But the results are identical. Now, with that little digression, I can go on to investigate the interplay of scope and custom directives, using the "$scope" way of declaring a controller.

Back to scope

A directive inherits the scope from its parents according to how it is configured. It is all governed by the value of a scope property on the directive definition object, which can have three possible values:

To experiment with these options, I created three directives, corresponding to these three options.

Scope.js ( function () { var app = angular.module('app', []); app.directive("shareScope", [function () { return { scope: false, template: "Shared scope.... S1: <input ng-model='One' /> S2: <input ng-model='Two' />", }; }]); app.directive("inheritScope", [function () { return { scope: true, template: "Inherited scope. I1: <input ng-model='One' /> I2: <input ng-model='Two' />", }; }]); app.directive("newScope", [function () { return { scope: {}, template: "New scope....... N1: <input ng-model='One' /> N2: <input ng-model='Two' />", }; }]); function MyController(scope) { scope.One = "One"; scope.Two = "Two"; } app.controller("myController", ["$scope", MyController]); } )();

All three directives output two input boxes to the DOM via the template property, binding to properties "One" and "Two". In addition, the controller sets two properties of the same names to default values. Now, the markup:

Scope.html <!doctype html> <html> <head> <title>Scope</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body style="font-family: 'Courier New', monospace;"> <div ng-app="app" ng-Controller="myController"> Controller scope C1: <input ng-model="One" /> C2: <input ng-model="Two" /> <div share-scope></div> <div inherit-scope></div> <div new-scope></div> </div> <script src="Scope.js"></script> </body> </html>

Explaining what happens and what the big difference is is a bit tricky, so I am including an iframe with the lot for you to experiment yourself right here, right now:

The above exercise demonstrates that when scope is set to be inherited (scope: true), there is still just the one scope, as long as it does not change in in an element produced by the inheriting directive. But as soon as that is changed, the bond is broken and a new scope is created. And that happens property by property. Finally, setting the scope to be isolated causes it to be completely independent in all cases.

This is not uncomplicated stuff and it took me a while to get my head around it, but I think I've got it now. But wait! What if there is more than one directive on the same element? Nothing prevents you from applying multiple directives to the same element, and those directives might declare disparate scope options. But an element can have only one scope. Angular resolves that according to the following scheme, involving two imaginary directives D1 and D2 on the same element:

A test with the two forbidden options gives an exception :

Error: [$compile:multidir] Multiple directives [inheritScope, newScope] asking for new/isolated scope on:....

New stuff

DDO.scope: Determines the scope of the directive. Options are shared with parent (false), isolated scope ( { } ) and inherited (true). If inherited, the directive will share a scope property with the parent until it is written to, in which case a child scope is created.

So, what scope options should you go for for a custom directive? The use of shared or inherited scope requires that the directive "knows" about the scope it is put into, i.e. the names and types of properties. This option is then adequate for custom directives that are specific to the domain or application. For more general purpose directives, the scope has to be isolated. For one reason, the directive can't know which context it is put in, and besides, you wouldn't want general purpose widgets to overwrite parent scope values.

Using isolated scope

As concluded above, one will often choose to use isolated scope in a directive in order not to risk messing up the parent scope. But there isn't much point in total isolation either; there has to still be a way to communicate things to and from a directive. Angular offers three different ways of doing that, cryptically named '@', '=' and '&' (remember, the skill of a developer is measured by the crypticism of his __code).

One-way data transfer with '@'

Let's first see an example of using the '@' syntax in the directive scope property and then disect it afterwards. First, the code for a directive named "isolated" that wishes to consume two pieces of data called name and value:

Isolated1.js ( function () { var app = angular.module('app', []); function IsolatedDirective () { return { scope: { value: "@", name: "@" }, template: "{{ value }} from {{ name }}", }; } app.directive("isolated", [IsolatedDirective]); function MyController() { var vm = this; vm.MyName = "Albert Einstein" vm.MyValue = "Hello world!" } app.controller("myController", [MyController]); } )();

In the markup, the two bits of data are passed to the directive in attributes with expressions:

Isolated1.html
<!doctype html> <html> <head> <title>Isolated</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body> <div ng-app="app" ng-Controller="myController as vm"> <div isolated value="{{ vm.MyValue }}" name="{{ vm.MyName }}"> </div> </div> <script src="isolated1.js"></script> </body> </html>
Name and value displayed
Name and value displayed

This example shows that data can be passed to a directive using attributes on the element. It requires isolated scope and setting the value of a named property in the directive scope to '@'. In the directive code, you are not forced to use the same name as the attribute. Say that you wanted to use the name privateName instead of name it would go like this:

Isolated1.js (partial) .... scope: { value: "@", privateName: "@name" }, template: "{{ value }} from {{ privateName }}", ....

New stuff

DDO.scope: { '@' }: Use the '@' syntax to establish a one-way binding between the isolated scope of a directive and the parent scope.

Using '@' data can be passed from the parent scope to the directive via an attribute, as we have just seen. It is even magically synchronized, so that when the value changes in the parent scope, the change is funnelled to the directive. Referring to the above example, if the value of vm.MyName were to change, it would propagate to the directive and the view would be updated. But the synchronization works only one-way. If you need to synchronize from the directive to the parent scope, you can use '=' instead.

Two-way data transfer with '='

Using the '=' syntax in isolated scope instead of '@' effectively establishes a two-way binding between scopes. The crucial difference in syntax is that in the attribute in markup, an expression is passed instead of a value (i.e. without the double curly braces), as such:

Isolated2.html (partial) .... <div ng-app="app" ng-Controller="myController as vm"> <div isolated value="vm.MyValue" name="vm.MyName"> </div> </div> ....

And in script:

Isolated2.js (partial) .... scope: { value: "=", name: "=" }, template: "{{ value }} from {{ name }}", ....

The result on screen is the same as the previous example: the two values are updated with the data from the controller scope, proving that data have flown to the directive. The promise of the '=' is that the value is synchronized in both directions between scopes. I would like to prove that by showing an example where the value in the directive scope is changed and then, supposedly, is propagated to the outer scope. But how does one change a value inside a directive? I have yet to devise a way to do that, so I am afraid I'll have to leave that example for now. I'll come back to it, though.

New stuff

DDO.scope: { '=' }: Use the '=' syntax to establish a two-way binding between the isolated scope of a directive and the parent scope.

Registering a callback with '&'

Instead of passing a value or an expression, a callback function can be passed to the directive by using the '&' syntax. This way, the directive is able to notify the parent scope of events such as when data changes or when a UI event occurs. This is reminiscent of the well-known control event pattern where a control notifies its container of an event.

For this example I have modified the directive to emit some markup with a button element. A handler for the click event on the button is set up with the ng-click directive. My directive expects an attribute named callme on the element, containing a callback function:

Isolated3.js ( function () { var app = angular.module('app', []); function IsolatedDirective () { return { scope: { callme: "&" }, template: "<button ng-click='callme()'>Click me!</button>", }; } app.directive("isolated", [IsolatedDirective]); function MyController() { var vm = this; vm.ClickCount = 0; vm.callback = function () { vm.ClickCount++; } } app.controller("myController", [MyController]); } )();

And the markup:

Isolated3.html
<!doctype html> <html> <head> <title>Isolated 3</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body ng-app="app"> <div ng-Controller="myController as vm"> <div isolated callme="vm.callback()"> </div> <div>Button has been clicked {{ vm.ClickCount }} times</div> </div> <script src="isolated3.js"></script> </body> </html>
Callback exemplified
Callback exemplified

The callback function is housed in the controller under the name vm.callback. In this example, when the button is clicked, the callme event handler is invoked, but that function is actually identical to vm.callback, because the callme attribute was assigned the value vm.callback(). The two are essentially aliases for the same function.

Calling back with parameters

The above example of using a callback was simpler than it might be in real life. Often the callback function will expect a number of arguments. So how do you handle that? It turns out there are at least two syntaxes for doing that, of which I will show one that I know works, including with a controller that uses the controllerAs syntax. But first, I must mention the DDO.controller property. Yes, a directive can have a controller, which is just a function that can embody properties and child functions, just as the "normal" controllers we have seen so far. The controller for a directive is the place to implement logic for the directive.

As was shown above, the directive must match the function name with the name of the attribute in markup. In the below example, I took the opportunity to verify that the attribute name data-callback-func matches the callbackFunc function name ("data-" gets stripped off and the rest gets camelCased). In the same way, the directive must use parameter names as they are stated in the attribute. The trick to getting this to work is to make the directive pass the arguments embedded in an object. For example, if the callback takes three parameters, the directive will pass an object with three properties, the names of which will match the parameter names defined in the attribute. As before, I have compiled an example, where the directive emits a button element to the DOM. When the user clicks the button the event argument is passed on to a callback in the parent controller:

isolated4.js: ( function () { var app = angular.module('app', []); function IsolatedDirective () { return { scope: { callbackFunc: "&" }, template: "<button ng-click='clickHandler( { event: $event } )'>Click me!</button>", controller: function ($scope) { $scope.clickHandler = function (ev) { $scope.callbackFunc({ param: ev.event, msg: "Hi from directive" }); }; }, }; } app.directive("isolated", [IsolatedDirective]); function MyController() { var vm = this; vm.ClickCount = 0; vm.CallMeOnClick = function (eventParam, greetingParam) { vm.ClickCount++; eventParam.target.innerHTML = greetingParam + " " + vm.ClickCount + " times"; } } app.controller("myController", [MyController]); } )();

And the accompanying markup:

Isolated4.html
<!doctype html> <html> <head> <title>Isolated 4</title> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> </head> <body ng-app="app" ng-Controller="myController as vm"> <div isolated data-callback-func="vm.CallMeOnClick(param, msg)"></div> <script src="isolated4.js"></script> </body> </html>
Callback with parameters
Callback with parameters

Let's try and follow the data flow from the button through the directive until it ends in the controller. When the button is clicked, the clickHandler in the directive controller is invoked, receiving a { event: $event } object. Then, the callback is called, which is really the CallMeOnClick function in the controller. You will see that a { param: ev.event, msg: "Hi from directive" } object is dispatched, but the CallMeOnClick function still receives two arguments; the dispatched object has been unpacked by Angular into two separate values.

The directive must use the parameter names as given in the attribute (here param, msg), but the parameter names in the controller can be anything (here eventParam, greetingParam).

New stuff

DDO.scope: { '&' }: Use the '&' syntax to establish a callback function in the parent scope for the directive to call.

Callback parameters:: The directive must pack parameters in an object and pass that.

DDO.controller:: A controller can be defined for the directive and is one place to implement logic for the directive.

Now, all three forms of communicating between directive and parent scopes have been demonstrated. Of course, a directive can use one, two or all three forms if need be.

< Previous chapterNext : Using and creating directives #2 >


2016 by Niels Hede Pedersen Linked in