Wednesday, November 7, 2012

Angular JS - Directive Basics

A while ago I posted some very basic information about AngularJS. There are a lot of really cool things to go over in Angular, but I think the most important thing to go over is probably directives. Directives are what tie everything together.


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-clickng-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.

12 comments:

  1. 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

    At 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

    ReplyDelete
    Replies
    1. 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).

      Delete
  2. Nice article, very clear!!
    how 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!

    ReplyDelete
    Replies
    1. 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.

      Delete
  3. Hey, 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.

    There 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


    ReplyDelete
    Replies
    1. 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.

      To 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

      Delete
  4. Yes, I see what you mean. I'm assuming that the reason for the new local scope is to define things in the narrowest scope possible, right? So if I had one controller with many directives, each directive could define it's local variables in it's local scope and the controller scope would be reserved for variables independent of the directives or variables needed by more than one directive. Is that what you're getting at? (As I said, I'm pretty much a total noob at Angular so feel free to correct me if I'm off base here. ;)

    Thanks again.

    ReplyDelete
    Replies
    1. Well isolating a scope to a directive is really a matter of the function of the directive. If your directive is complex and manipulating data that is being displayed only within that directive, you'll probably want to isolate the scope. One, to keep it narrow, and two, to prevent the parent scope from getting junked up. However, there are some cases were scope isolation is unnecessary, or even undesirable... a validation directive, for example.

      Delete
  5. For anyone else who's interested, here are a couple of SO posts on directive/controller options.
    http://stackoverflow.com/questions/15672709/how-to-require-a-controller-in-an-angularjs-directive
    http://stackoverflow.com/questions/15622863/angularjs-directive-controllers-requiring-parent-directive-controllers

    ReplyDelete
  6. Nice post, i like this.

    Btw, your blog description is great.

    ReplyDelete
  7. So, if I understood correctly, directives aren't supposed to interact with the rest of your application. That is to say, they are like mini self-contained and self-sufficient app within you app with very specialized, single-minded functionality ... correct?

    ReplyDelete
    Replies
    1. The most important thing about directives is that they are the touch point between your application and the DOM. Nothing else should really be touching the DOM at all. That is what directives are for. So for binding DOM events, or for writing things to the DOM, or for getting data from the DOM and putting it in your scope... directives.

      Delete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)