Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Ben Alman
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Ben Alman@cowboy )

Directive Controllers Can Use Dependency Injection In AngularJS

By Ben Nadel on

Over the weekend, I read a very thought provoking post by Tero Parviainen on removing ngController from his AngularJS applications. And, while I am still "digesting" his approach, I must admit that his code pointed out an AngularJS feature that I had not see before - directive controllers can be defined in the AngularJS dependency injection container, just like any other controller.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Most of the time, when I build a directive that needs a Controller, I just define the Controller constructor function inside the Directive constructor function. Then, in the directive configuration, I simply provide a direct reference to the controller. But, as Tero pointed out in this code, the directive configuration can pull a controller out of the dependency injection container.

While I am not well versed in testing AngularJS code, this approach would make the directive controller testable outside of the directive context. But more than that, it may also make the organization around the code a bit more flexible and modular.

Anyway, since I didn't know this was possible, I wanted to put together a small demo to see it in action. In the following code, you'll see that my directive configuration references a controller defined at the key, "my.nameSpace.TestController". You'll also see that the given Controller can accept dependency-injected arguments, just like any other controller:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Directive Controllers Can Use Dependency Injection In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Directive Controllers Can Use Dependency Injection In AngularJS
  • </h1>
  •  
  • <div bn-test-directive>
  • Testing this directive.
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I define a Controller in the dependency injection container. I can be bound
  • // to an HTML element node using either ngController of as a directive controller.
  • app.controller(
  • "my.nameSpace.TestController",
  • function( $scope, $timeout, $q ) {
  •  
  • console.info( "Controller instantiated" );
  •  
  • // Use $timeout to show that dependency injection worked.
  • var timer = $timeout(
  • function handleTimeout() {
  •  
  • return( "Timer executed" );
  •  
  • },
  • 500
  • );
  •  
  • // Use $q to show that dependency injection worked.
  • $q.all( [ timer ] ).then(
  • function handleTimerResolve( resolvedValues ) {
  •  
  • console.log( "Resolution:", resolvedValues[ 0 ] );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I bind some JavaScript UI behavior to a given scope. And, in this case, I am
  • // also binding a Controller to a given UI as well.
  • app.directive(
  • "bnTestDirective",
  • function() {
  •  
  • // I bind the UI behaviors to the local scope.
  • function link( scope, element, attributes, controller ) {
  •  
  • console.info( "Directive linked" );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • // --
  • // NOTE: We are providing the Controller name as a value to be pulled out
  • // of the dependency injection container.
  • return({
  • controller: "my.nameSpace.TestController",
  • link: link,
  • require: "bnTestDirective",
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the directive controller looks just like any other controller. In fact, there's nothing in the directive controller that would indicate that it was being used for a custom directive and not for the native ngController directive.

When we run the above code, we get the following output:

Controller instantiated
Directive linked
Resolution: Timer executed

The code in the controller is mostly non-sense - I just wanted to make sure that the controller arguments integrated properly with the dependency injection framework.

Very few of my directives actually use a Controller. And, those that do use a controller often use it to help manage DOM-related features (ex. transclusion, image loading); as such, I am not sure that I believe that directive controllers and "normal controllers" are truly the same beast. But, I am also not going to draw any conclusion just yet. More noodling is required.




Reader Comments

Wouldn't it be better to DI the directive function explicitly?

```
app.directive('myFoo', ['myController', function (myController) {
return {
restrict: 'E',
controller: myController
};
}]);
```

Reply to this Comment

@Gleb,

Yeah, if the injected values need to be exposed to the Link and/or Compile functions (or any other function inside the directive factory). But, in this case, the Controller is defined outside of the directive factory and therefore would need its own DI integration.

Of course, like I said, this is the first time I even realized you could do this; in the past, I have always defined my directive-controller inside the directive factory and, as you are saying, have always used the directive factory to handle the injection.

Reply to this Comment

I think it works exactly the same way as every others Controllers.
It's usefull if you want to use the same Controller in multiple Directives.

Reply to this Comment

@Bertrandg,

Word up. For some reason, though, I always had this mental model that they were "different" controllers. That there were "Controller" controllers and "Directive" controllers, and that they were the same in name only.

Funny how that happens - you get a story in your head and forget to question it. Then, so much time goes by and blam! You're wrong :D

Reply to this Comment

@Ben,

The controller is already injected into the link function as the fourth argument -- unless you `require` other controllers. In any case, if you're using that, I would list the directive's own controller explicitly in `require` anyway so it is clear what is happening.

Reply to this Comment

@Vincent,

It's funny you mention that - I actually discovered that accidentally the other day. It's in the documentation... but there's *so much* documentation, it's hard to absorb it all. Anyway, I was writing some code and forgot to add the "require" attribute; when I realized it wasn't there, I was confused as how the Controller was actually being injected.

Reply to this Comment

@Ben,

I know what you're saying - hopefully it will be easier to grasp when Angular 2 is finally there :)

Reply to this Comment

@Ben @Gleb @Vincent,

Yup, a recent catharsis, for me as well, while trying to push all code to directives instead of controllers ( lesser 2.0 migration so "Father-G" says :p ).

Learner Question: If it is the same (beast), should i pick the one implementation which causes less maintenance/references like controllers-directive ? is there any performance/testing drawbacks that you can think of?

Best Regards
Jorge

Reply to this Comment

@Jorge,
I prefer custom directives with isolate scopes - makes each part of the page pretty well defined and simple to understand since there is less coupling. As far as testing / performance: we try to keep number of things a directive needs to minimum (number of injected dependencies + scope properties + $on events).

For testing, take a look at https://github.com/kensho/ng-describe - it has directive testing support including parent scope.

Reply to this Comment

My team has written several directives this way for the ease of unit testing. After upgrading to 1.3 the fourth link param, the controller, is not passed in. Is this no longer supported? I didn't see anything to this effect in the list of breaking changes.

Reply to this Comment

How to make controller known dynamically?
in this part:

controller: "my.nameSpace.TestController",

I want to set this controller up by passing it's name via $scope for example

Reply to this Comment

Thanks Ben! Just gone thru a namespacing exercise, using dots, and then injecting namespaced services into the controller requires the DI syntax, but when the controller function is declared inside of the directive the minimiser is going to brush it all aside...

Therefore your approach of a separate controller is great and just the job in this situation. Thanks!

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.