Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Sebastian Zartner
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Sebastian Zartner

Directive Controllers Can Use Dependency Injection In AngularJS

By
Published in Comments (14)

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

19 Comments

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

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

15,848 Comments

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

7 Comments

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.

15,848 Comments

@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

26 Comments

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

15,848 Comments

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

5 Comments

@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

19 Comments

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

1 Comments

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.

1 Comments

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

1 Comments

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!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel