Friday, November 22, 2013

Flexible directive names in AngularJS

AngularJS is an HTML compiler. Because of this the most powerful component in Angular is the directive.

The first thing I noticed when using directives in Angular was how they were made available to the application. To use a directive they have to be named and added to a module under that name. You can then use that name to call the directive. When using a directive you usually want to pass in some information to be used. This can be done by setting the directive name as an attribute on the element and then passing an expression to it. This means you have code like the following:


<div my-directive="myExpression" />


So all I need to do is parse the expression and I have my information. This leads to directive code like the following:


var myDirective = function($parse) {
	return {
		link: function(scope, element, attrs) {
			var myInfo = $parse(attrs['myDirective'])(scope);
		}
	}
};


I've hard coded in 'myDirective', but what happens if that directive wants to be used under another name? Well the easy fix is to attach that directive to a module and enforce the name of the directive like so:


var myDirectiveModule = angular.module('myDirectiveModule', [])
	.directive('myDirective', myDirective);


now the end user needs to pull in the module as a dependency and the directive wil be saved under 'myDirective'.

But what if we want the user to define what the directive is called?

Well it turns out we can find out how the directive was registered because of this code in angular js:


  /**
   * @ngdoc function
   * @name ng.$compileProvider#directive
   * @methodOf ng.$compileProvider
   * @function
   *
   * @description
   * Register a new directive with the compiler.
   *
   * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which
   *    will match as ng-bind), or an object map of directives where the keys are the
   *    names and the values are the factories.
   * @param {function|Array} directiveFactory An injectable directive factory function. See
   *    {@link guide/directive} for more info.
   * @returns {ng.$compileProvider} Self for chaining.
   */
   this.directive = function registerDirective(name, directiveFactory) {
    assertNotHasOwnProperty(name, 'directive');
    if (isString(name)) {
      assertArg(directiveFactory, 'directiveFactory');
      if (!hasDirectives.hasOwnProperty(name)) {
        hasDirectives[name] = [];
        $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
          function($injector, $exceptionHandler) {
            var directives = [];
            forEach(hasDirectives[name], function(directiveFactory, index) {
              try {
                var directive = $injector.invoke(directiveFactory);
                if (isFunction(directive)) {
                  directive = { compile: valueFn(directive) };
                } else if (!directive.compile && directive.link) {
                  directive.compile = valueFn(directive.link);
                }
                directive.priority = directive.priority || 0;
                directive.index = index;
                directive.name = directive.name || name;
                directive.require = directive.require || (directive.controller && directive.name);
                directive.restrict = directive.restrict || 'A';
                directives.push(directive);
              } catch (e) {
                $exceptionHandler(e);
              }
            });
            return directives;
          }]);
      }
      hasDirectives[name].push(directiveFactory);
    } else {
      forEach(name, reverseParams(registerDirective));
    }
    return this;
  };


This is how a directive is registered and where a name is passed in. You can also see this line:


directive.name = directive.name || name;


Which basically says that if no name is provided on the directive object (the object we return from the directive factory) then we set it to the one it is registered with. This means that if we keep a reference to that object then we can use that name variable like so:


var myDirective = function($parse) {
  var directiveObj = {
    link: function(scope, element, attrs) {
      var myInfo = $parse(attrs[directiveObj.name])(scope);
    }
  };

  return directiveObj;
};


Now we have a directive that can be assigned to a module under any name.

No comments:

Post a Comment