Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Josh Knutson

Using Namespaces For One-Off Directives In AngularJS

By Ben Nadel on

A few months ago, I blogged about delegating directive-linking functions up the DOM tree to a parent directive. The motivation behind that blog post was to be able to create one-off directives that didn't need globally-unique names within the dependency-injection mechanism provided by the AngularJS module. While the link-delegation did solve that problem, it didn't feel right; I think what I really wanted was to be able to name-space my one-off Directives in the same way that I name-space my one-off Controllers.


 
 
 

 
  
 
 
 

Many directives are meant to be reused in a variety of contexts. The built-in AngularJS directives, such as ngClick, ngMouseenter, and ngRepeat, are examples of directives that can be easily reused. This blog post is not about those kind of directives. Rather, this exploration is tailored specifically for one-off directives that are intended to be "consumers" of a given Controller.

In such a context, it would be helpful if both the Controller and the relative Directives could be namespaced. Unfortunately, AngularJS only provides a mechanism for namespacing Controllers. This is probably because a directive requires both an attribute name and an (optional) attribute value, which doesn't lend well to namespacing.

But, with a one-off directive, that is intended for a specific context, we have fewer requirements when it comes to flexibility. As such, we can come up with a way to define a one-off Directive that more closely mirrors a one-off Controller:

  • ng-controller="namespace.for.my.Controller"
  • bn-directive="namespace.for.my.Directive"

Here, we are moving the identifier of the Directive from the attribute name to the attribute value. In doing so, we are allowing our directive identifiers to have any string-based value, including a dot-delimited, namespaced location.

These directives still have to have a globally-unique name within a given dependency-injection context; however, unlike typical directives, they lend themselves more naturally to a hierarchical organization. To see this in action, the following demo showcases a Controller and a Directive that are both defined within the "demo" namespace:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Using Namespaces For One-Off Directives In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • ul.items {
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px ;
  • }
  •  
  • ul.items li {
  • background-color: #FAFAFA ;
  • border: 1px solid #CCCCCC ;
  • border-radius: 5px 5px 5px 5px ;
  • cursor: pointer ;
  • margin: 0px 0px 0px 0px ;
  • padding: 6px 8px 5px 7px ;
  • position: fixed ;
  • white-space: nowrap ;
  • z-index: 2 ;
  • }
  •  
  • ul.items li:hover {
  • border-color: #FF3399 ;
  • border-width: 2px 2px 2px 2px ;
  • box-shadow: 2px 2px 3px #AAAAAA ;
  • margin: -1px 0px 0px -1px ;
  • z-index: 3 ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • Using Namespaces For One-Off Directives In AngularJS
  • </h1>
  •  
  • <!--
  • This UL class defines both a Controller and a "directive"
  • which both use the "demo." namespace. The bnDirective
  • attribute allows me to define one-off directives using a
  • namespace in the same way I define controllers.
  • -->
  • <ul
  • ng-controller="demo.ListController"
  • bn-directive="demo.ListDirective"
  • class="items">
  •  
  • <li
  • ng-repeat="item in items"
  • ng-click="removeItem( item );"
  • ng-style="{ left: ( item.x + 'px' ), top: ( item.y + 'px' ) }">
  •  
  • ( {{ item.x }}, {{ item.y }} )
  •  
  • created on
  •  
  • {{ item.label }}
  •  
  • </li>
  •  
  • </ul>
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-2.0.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the list of items.
  • app.controller(
  • "demo.ListController",
  • function( $scope ) {
  •  
  • // I am the initial list of items.
  • $scope.items = [
  • {
  • x: 100,
  • y: 100,
  • label: "Created at initialization"
  • },
  • {
  • x: 75,
  • y: 180,
  • label: "Created at initialization"
  • }
  • ];
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I add a new item to the current collection.
  • $scope.addItem = function( x, y ) {
  •  
  • $scope.items.push({
  • x: x,
  • y: y,
  • label: getItemLabel()
  • });
  •  
  • };
  •  
  •  
  • // I remove the given item from the current collection.
  • $scope.removeItem = function( item ) {
  •  
  • var index = $scope.items.indexOf( item );
  •  
  • $scope.items.splice( index, 1 );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I define the created-at label for a new item.
  • function getItemLabel() {
  •  
  • var now = new Date();
  • var hours = now.getHours();
  • var minutes = now.getMinutes();
  • var seconds = now.getSeconds();
  • var time = ( hours + ":" + minutes + ":" + seconds );
  •  
  • return( "Created at " + time );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I define the directive at the given namespace. While this
  • // is a "controller", it is really an object being instantiated
  • // during the LINK phased of the bnDirective directive.
  • app.controller(
  • "demo.ListDirective",
  • function( $scope, element, attributes, $document ) {
  •  
  • // Listen for click events on the document so that we
  • // can create new items, when appropriate.
  • $document.on(
  • "click.demoListDirective",
  • function( event ) {
  •  
  • // If the click target was an LI, ignore it -
  • // the core controller will handle it.
  • if ( $( event.target ).is( "li" ) ) {
  •  
  • return;
  •  
  • }
  •  
  • // "Consume" the addItem() method in the
  • // parent controller.
  • $scope.$apply(
  • function() {
  •  
  • $scope.addItem(
  • ( event.pageX - 45 ),
  • ( event.pageY - 12 )
  • );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // When the $scope is destroyed, we have to make
  • // sure to unbind the event handler so we don't get
  • // unexpected behaviors.
  • $scope.$on(
  • "$destroy",
  • function() {
  •  
  • $document.off( "click.demoListDirective" );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I invoke the Controller defined by the "bnDirective"
  • // attribute value during the linking phase of the current
  • // directive instance. This is intended to be used with
  • // one-off directives that are defined using namespacing.
  • app.directive(
  • "bnDirective",
  • function( $controller ) {
  •  
  • // I bind the $scope to the JavaScript UI events.
  • function link( $scope, element, attributes ) {
  •  
  • // Instantiate the given controller, using the
  • // link arguments to define the local injector
  • // argument values.
  • $controller(
  • attributes.bnDirective,
  • {
  • element: element,
  • $scope: $scope,
  • attributes: attributes
  • }
  • );
  •  
  • }
  •  
  •  
  • // I return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

When using this approach, I am technically defining the one-off directive as an AngularJS controller; however, this pseudo-controller is injected with the same $scope, element, and attributes value that a normal link function would receive. In fact, this pseudo-controller is - for all intents and purposes - the link function for the one-off directive.

The tunneling that allows this to happen is the bnDirective directive which instantiates the pseudo-controller/one-off directive using the namespaced identifier. This directive then passes the link-function arguments through as injectables to the one-off directive Controller instance. At this point, the directive pseudo-controller can then bind to JavaScript UI events the same way any other directive would.

Before I finish, I should note that, while this entire demo is in a single file, the intent of the namespacing is to more closely mirror the underlying directory structure (the way a Java namespace mirrors the internal JAR structure). So, the files for the above demo would likely be something like this:

  • / app / controllers / demo / list-controller.js
  • / app / controllers / demo / list-directive.js
  • / app / directives / directive.js

In this case, I am putting the "ListController" and the "ListDirective" in the same folder (and therefore namespace) because I am viewing the ListDirective as a sort-of UI-based "controller extension" for the ListController; it consumes the ListController, providing the UI-bindings that a normal AngularJS controller wouldn't define.

Clearly, the use for this kind of mechanism is limited in scope. For example, the bnDirective directive doesn't account for non-attribute values, transclusion, or terminal execution (to name a few AngularJS directive features). However, with the way that I structure my AngularJS applications, being able to keep my one-off Directives closer to my one-off Controllers feels very natural; your mileage may vary.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Hy Ben, thank you for that explanation and nice code. I often refer to your articles (almost every day) and that one was really helpful.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.