Directive Controllers Can Use Dependency Injection In AngularJS
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.
Want to use code from this post? Check out the license.
Reader Comments
Wouldn't it be better to DI the directive function explicitly?
```
app.directive('myFoo', ['myController', function (myController) {
return {
restrict: 'E',
controller: myController
};
}]);
```
@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.
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.
@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
@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.
@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.
@Ben,
I know what you're saying - hopefully it will be easier to grasp when Angular 2 is finally there :)
@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
@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.
@Jorge,
I personally still use ngController and I use non-scope directives mostly. I'd be cautious about "defaulting" to creating isolate scopes:
www.bennadel.com/blog/2729-don-t-blindly-isolate-all-the-scopes-in-angularjs-directives.htm
Unless you are creating directives that actually do things like transclude content, an isolate scope is likely to cause problems down the road (if not sooner).
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.
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
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!
@Dmitri,
This is kind of late, but might be useful for you.
https://toddmotto.com/dynamic-controllers-in-directives-with-the-undocumented-name-property/