One of the main points of angular consists on adding functionalities to your html elements, so that instead of setting the effects the jQuery way: selecting the element and making that happen or adding a listener, you create an “effect”, “functionality” or “behaviour” and give it a name, and then assign it to an element by just writing it on the View. No need to access the view from the controller, only to set the directive. Let’s see a quick example:
var app = angular.module('myApp', []); app.directive('helloWorld', function ($compile) { return { restrict: 'AE', replace: 'true', template: "<h1>Hello World!</h1>" } });
Now just add this HTML:
<html ng-app> <body> <hello-world></hello-world> </body> </html>
That example creates a directive named “helloWorld”, and we are adding it to our view by just including a new tag with the same name as the directive. This directive just prints “Hello World!”, replacing the “hello-world” tag, but directives can do much more. For example, you can use the property “templateUrl” to get the template from somewhere else, not use any template at all, modify the scope before it’s printed to the element or the DOM after the scope it’s printed, access its generated children, create children and add them, etc.
Templates
I just showed you how to use the template property, but you can also use the templateUrl property to get a template from elsewhere, as a view. Like this:
<html> <body> <my-angular-app ng-app></my-angular-app> <script> var app = angular.module('myApp', []); app.directive('myAngularApp', function ($compile) { return { restrict: 'AE', replace: 'true', templateUrl: "myTemplate.html" } </script> </body> </html>
And that would replace the “my-angular” tag with our view. Note that I’ve set ng-app on the custom tag, not on the html one, this allows us to add apps to our html code with a mere custom tag and a call to a script were the application resides (if you have many scripts, you can use RequireJS).
Also, you can use a function with templateUrl which will allow you to set the view url dynamically as explained on angular docs:
<div ng-controller="Controller"> <div my-customer></div> </div>
angular.module('docsTemplateUrlDirective', []) .controller('Controller', ['$scope', function($scope) { $scope.customer = { name: 'Naomi', address: '1600 Amphitheatre' }; }]) .directive('myCustomer', function() { return { templateUrl: function(elem, attr){ return 'customer-'+attr.type+'.html'; } }; });
Note that you can’t access scope variables from the templateUrl function, since the template is requested before the scope is initialized.
Calling the directive: restrict
Let’s now pay some detail to how the directive is named and called so that you can make you owns. Angular will automatically transform the camel cased name of a directive into a snake case when we call it on the HTML. So our “helloWorld” becomes “hello-world”. Angular doesn’t mind if the HTML call is upper or lower cased (it’s case insensitive) but it does if the diretive is declared starting with a capital, so you will have to set the first word in lower case.
Once we have the name, there are different ways to call the directive, not just the one I showed, let’s see them using the hello-world example:
A. <div hello-world></div> C. <div class="hello-world"></div> E. <hello-world></hello-world> M. <!-- directive: hello-world -->
Yes, you can also use a comment to set a directive. I’ve given them a letter to explain how to use the property restrict as I’m here. Have a look again at how the directive is created and you will notice that there is a property called “restrict”. Such property allows to specify which ways of calling a directive are allowed, so if you set “ACEM” you allow them all, but if you set “AC” you are only allowing those two ones.
Controller
By setting the controller property you can have a mini-controller (well, if it’s mini or not will depend on what you write in there :D) with its own scope, and injected dependencies that will be called before the model is printed into the View. Let’s see an example:
<html> <body ng-app> <helloworld>{{Greeting}}</helloWorld> <script> var app = angular.module('myApp', []); app.directive('helloWorld', function () { return { restrict: 'E', controller: function($scope, $element){ $scope.Greeting = "Hello World!"; } } </script> </body> </html>
In this example I’ve created another helloWorld directive that will set a value to the scope, which is the application’s main scope injected into the directive, though in case we are in a sub-scope (like an ng-repeat) that’s what it will be injected into the directive. Once the scope has the value we need, look how the HTML is expecting such thing to be in the scope to paint it.
But we can also access the element, the DOM object, by using the injected $element. This way you have full control of it and can read its properties, check its children or anything you may need to read. Have in mind that when in the Controller the Model has not been painted into the View yet, so be careful with what you expect to read.
Finally, as it’s a effectively a controller, we can also add other dependencies to it without any problem, like the $http or $log:
app.directive('helloWorld', function () { return { restrict: 'E', controller: function($scope, $element, $http, $log){ $scope.Greeting = "Hello World!"; } }
And that will work as nicely as your main controller.
Link
Following the “life-cycle” of your Angular’s application, there’s a point in which angular will paint the Model into the View, and you may want to access what’s been painted to modify it in any way. To do that we can use the property “link”, which is very similar to the controller but with no dependency injection and being executed after the View is painted:
<html> <body ng-app> <helloworld></helloWorld> <script> var app = angular.module('myApp', []); app.directive('helloWorld', function () { return { restrict: 'E', link: function(scope, element, attr){ element.html("Hello World!"); } } </script> </body> </html>
As link is allowing you access to the DOM element once painted, you can take advantage of that to access custom properties that modify some value, even if this happens after painting the DOM it should be quick enough to be there before the user notices:
<html> <body ng-app> <helloworld customValue="Hola mundo!"></helloWorld> <script> var app = angular.module('myApp', []); app.directive('helloWorld', function () { return { restrict: 'E', link: function(scope, element, attr){ var value = attr.customValue; element.html(value); } } </script> </body> </html>
Scope
By default, directives inherit their parent scope so they have access to anything on the parent. While this is quite useful you may prefer the directive to have no access to it or a restricted one, to do that you only need to set a new scope to the directive like this:
app.directive('helloWorld', function () { return { scope: {} }
That will create an empty scope for the directive blocking it access to the parent and isolating it. If you want to give access to something in the parent scope you need to declare it on this scope. You can do that using an @ to set it as read-only property:
app.controller("myController", function(){ $scope.parentProperty = "123"; }); app.directive('helloWorld', function () { return { scope: { parentProperty: "parentProperty", parentPropertyRenamed: "parentProperty", readOnlyProperty: "@parentProperty" // notice the @!! } }
This can help on isolating directive from the main controller but if you want to fully isolate it by making it not know what the parent has at all you can use a trick like this, set a property on the directive element to get a value from the parent:
<html> <body ng-app ng-controller="myController"> <map coordinates="theCoordinates"></map> <script> var app = angular.module('myApp', []); app.controller("myController", function(){ $scope.theCoordinates = "4,5"; }); app.directive('map', function () { return { restrict: 'AE', scope: { coordinates: '=coordinates' //set the equal! }, template: "<gmap coords='{{coordinates}}'> ... </gmap>" } </script> </body> </html>
Which will declare a map directive that expects to have a map property linking to a parent scope property to get the value. Do notice that if a different controller where to use the directive both controllers don’t need to use the same scope.property, as they can say where to find the coordinates inside their scope by using the “coordinates” property in the directive. This allows independency to the directive from the controller using it.
You can also “inherit” a method instead than a value using “&”:
<html> <body ng-app ng-controller="myController"> <map coordinates="getCoordinates()"></map> <script> var app = angular.module('myApp', []); app.controller("myController", function(){ $scope.getCoordinates = function(){ return "4,5"; } }); app.directive('map', function () { return { restrict: 'E', scope: { coordinates: '&coordinates' //set the &! }, template: "<gmap coords='{{coordinates}}'> ... </gmap>" } </script> </body> </html>
Bibliography and many thanks to
ng-newsletter – Build custom directives with AngularJS
Sitepoint – A Practical Guide to AngularJS Directives.
Jason More – Difference between controller and link.