Where the rubber meets the road
Directives in angular are easily the most powerful and complicated peace to the puzzle. Directives are used to set up DOM manipulations, interactions between the DOM and the scope, and a great many other things. Examples of directives are all over the Angular core framework. ng-model, ng-click, ng-repeat, ng-app are all examples of directives. Even the select, textarea and input tags have been extended as a directive. Directives can be used to set up JQuery plugins, do validation, create custom reusable controls.
Directives come in many different flavors
- Elements - such as <my-directive>expression here</my-directive>
- Attributes - such as <div my-directive="expression here"></div>
- Classes - such as <div class="my-directive: expression here;"></div>
- Comments - such as <!-- directive: my-directive expression here -->
All of the above examples could even be the exact same directives used differently.
The anatomy of a directive
Warning: the following example is contrived, and really silly. But I'm trying to illustrate the most commonly used pieces of a directive declaration.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
//the following will declare a new directive that
// may be used like <my-directive name="foo"></my-directive>
// where foo is a property on a controller's scope.
app.directive('myDirective', function(){
// The above name 'myDirective' will be parsed out as 'my-directive'
// for in-markup uses.
return {
// restrict to an element (A = attribute, C = class, M = comment)
// or any combination like 'EACM' or 'EC'
restrict: 'E',
scope: {
name: '=name' // set the name on the directive's scope
// to the name attribute on the directive element.
},
//the template for the directive.
template: '<div>Hello, {{name}} <button ng-click="reverseName()">Reverse</button></div>',
//the controller for the directive
controller: function($scope) {
$scope.reverseName = function(){
$scope.name = $scope.name.split('').reverse().join('');
};
},
replace: true, //replace the directive element with the output of the template.
//the link method does the work of setting the directive
// up, things like bindings, jquery calls, etc are done in here
link: function(scope, elem, attr) {
// scope is the directive's scope,
// elem is a jquery lite (or jquery full) object for the directive root element.
// attr is a dictionary of attributes on the directive element.
elem.bind('dblclick', function() {
scope.name += '!';
scope.$apply();
});
}
};
});
The above directive is a crude example. It will output a "Hello, World" statement with a button to reverse the name with just the following markup: <my-directive name="name"></my-directive>, presuming the parent scope has a property name equal to "World". It will also set up a double-click event that will tack an exclamation point on the end of the name.
And here's my absurd directive in action:
Fears of "Custom HTML tags" are unfounded
Have no fear. Angular is not destroying your perfect markup. Angular is using them as placeholders, nothing more. The HTML spec itself even says that custom tags should be ignored. If you're using "replace: true" in your directives, it's all replaced by whatever HTML you put in the template anyhow. This is a common complaint I've heard about Angular, and it's just a bad reason not to at least try Angular. It's an incredibly fun and powerful tool.
Directive Tips & Gotchas
- Use the existing directives to do your event binding if possible. Don't bind events with JQuery anymore, just stop it. Also, DO NOT do what I did in my example and create your own simple binding like "dblclick", there is already a directive
- You can nest directives. A directive's template may contain other custom directives.
- Put them in their own module. Generally, it's a good idea to organize your directives into their own module. This promotes reuse in other modules as well as a separation of concerns.
- I've witnessed self-closing directive tags not function properly. Always use both the open and close tags for your element directives.
For more comprehensive information about directives, have a look here.
Very nice write up! -- I want to ask you whether you've been able to use the camelcase for calling directives in HTML tags attrs. Like span myDirective /span
ReplyDeleteAt least in the latest Chromes, I haven't been able to use myDirective to call a directive, i have to use my-directive. I haven't tried them out in FF or Safari
I honestly haven't tried that. I've only used the dash-case tag names. As I prefer them (it saves my pinkies a little shifting).
DeleteNice article, very clear!!
ReplyDeletehow to declare 'MainCtrl' in routeprovider, because it is in module, i cant pass the control there, as
when('/protectedSystems', {templateUrl: 'partials/protectedSystems.html', controller: MainCtrl}).
i want to use the controller in this page, how can access if i do like this??
am very new to this directives, yesterday itself i started. hope for quick reply. Thank you!
shanthi, in the case of MainCtrl, that declaration would be in your main application module, not your directive module. The main application module's config would be where you'd be setting up your routeProvider. In my example above, I was putting everything in a single module just to simplify things.
DeleteHey, thanks for some very helpful info, very well-presented. I wish more people would include some simple, well-commented, code that shows how to produce a simple result. This makes it ideal for someone else to use it as a starting point. That's a big help, especially with a fairly complicated framework like Angular.
ReplyDeleteThere was one thing I was still curious about after reading this. How could I convert this to the 'shorthand' syntax for directives, where you only specify the linking function and leave all the other values as defaults? I'm an Angular noob so it took a while, but the code below works. I'm sure it could be improved but I thought I'd share it in case anyone else was interested.
There were two tricky parts to the conversion. First, the Angular default is that your directive will be an attribute. In your 'long-form' directive definition, you defined it as an element. Before I figured this out, Angular wouldn't call my linking function. No error messages, unfortunately, so that took a while. Once I changed the directive to an attribute, Angular started calling my linking function.
The second tricky part was moving your template html to my linking function. Because the html contains Angular directives like {{name}} and 'ng-click', it must be compiled before it's inserted into the DOM. The syntax is a little funny-looking: "elem.append($compile(newhtml)(scope));" The $compile function returns a linking function which is then called on the element's scope. This returns an Angular object that can be inserted into the DOM with elem.append().
Code is below. Thanks again for your article.
------------------------------------------------------------------------------------
http://plnkr.co/edit/2jV41rN0v0YqgEJlQENU
I'm glad you liked the article, and it's cool that you tried your hand at the shorthand version of the directive. The biggest thing I would note about your code is that in what you've done, the directive has become dependant on the parent scope containing the methods for reverseName() and ep(), whereas in the long form, they were defined in the controller for the directive on the directive's scope.
DeleteTo fix this, you would define those methods on a local scope that you'd have to create yourself in your directive.
Here's what I'm talking about: http://plnkr.co/edit/CNliWSMAvEWPbm9kpso7?p=preview