Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Eric Betts
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Eric Betts

Exploring Directive Controllers, Compiling, Linking, And Priority In AngularJS

By
Published in Comments (9)

Recently, I talked about using Controllers inside of AngularJS Directives as a way to facilitate inter-directive communication. For simple directives, this seems to work great; however, I recently ran into a wall when trying to use directive controllers in conjunction with a directive that also had a compile() function. In my case, a non-compile directive couldn't "require" the controller defined in the compiled directive, even if they were applied to the same DOM element.

AngularJS directives are super powerful, but, they are also a total mind-blow; so, to say that they are difficult to understand [deeply] would be an understatement. When I ran into the problem above, I realized that I needed to do a good deal more exploration when it came to directives that explicitly hook into the compile-phase of the compile-link lifecycle.

Timing in AngularJS is a tricky beast. So, for this exploration, I wanted to look at the ability for both sibling and descendant directives to access (ie. require) each other's Controllers. In the following code, I have four sibling directives and one descendant directive. Of the four sibling directives, two execute with higher priority. And, of the two with higher priority directives, one uses a compile function:

  • bnOne - priority 500. Compiles.
  • bnTwo - priority 500.
  • bnThree - priority 400.
  • bnFour - priority 400.
  • bnChild - nested directive.

Before we look at the compile version, however, let's run a control case in which the structure and priorities are the same, but none of the directives hook into the compile phase.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Exploring Directive Controllers, Compiling, Linking, And Priority In AngularJS
	</title>
</head>
<body>

	<h1>
		Exploring Directive Controllers, Compiling, Linking, And Priority In AngularJS
	</h1>

	<p bn-one bn-two bn-three bn-four>

		<span bn-child>Checkout my controllers!</span>

	</p>



	<!-- Load jQuery and AngularJS from the CDN. -->
	<script
		type="text/javascript"
		src="//code.jquery.com/jquery-1.9.0.min.js">
	</script>
	<script
		type="text/javascript"
		src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
	</script>

	<!-- Load the app module and its classes. -->
	<script type="text/javascript">


		// Define our AngularJS application module.
		var demo = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 500.
		demo.directive(
			"bnOne",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnOne";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnOne ( priority: 500 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 500,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 500.
		demo.directive(
			"bnTwo",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnTwo";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnTwo ( priority: 500 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 500,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 400.
		demo.directive(
			"bnThree",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnThree";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnThree ( priority: 400 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 400,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 400.
		demo.directive(
			"bnFour",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnFour";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnFour ( priority: 400 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 400,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I am the child directive
		demo.directive(
			"bnChild",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnChild";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnChild" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the PARENT
				// element.
				return({
					controller: Controller,
					link: link,
					require: [ "?^bnOne", "?^bnTwo", "?^bnThree", "?^bnFour" ],
					restrict: "A"
				});

			}
		);


	</script>

</body>
</html>

As you can see, the P tag has four directives applied to it, and one directive applied to its descendant Span tag. Each of these directives "requires" the controllers of the four sibling directives. Then, in the linking phase, each directive logs the controllers to which it was able to gain reference.

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

AngularJS directive controllers being injected into other directives.

As you can see, each link() function was injected with the four directive controllers that it required. This was irrelevant of priority and DOM placement.

Now that we see how easily this works with non-compile directives, let's step into the danger zone and look at a version in which the bnOne directive needs to execute a compile() function and then a subsequent transclude() function. Note that none of the other directives have changed at all:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Exploring Directive Controllers, Compiling, Linking, And Priority In AngularJS
	</title>
</head>
<body>

	<h1>
		Exploring Directive Controllers, Compiling, Linking, And Priority In AngularJS
	</h1>

	<p bn-one bn-two bn-three bn-four>

		<span bn-child>Checkout my controllers!</span>

	</p>



	<!-- Load jQuery and AngularJS from the CDN. -->
	<script
		type="text/javascript"
		src="//code.jquery.com/jquery-1.9.0.min.js">
	</script>
	<script
		type="text/javascript"
		src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
	</script>

	<!-- Load the app module and its classes. -->
	<script type="text/javascript">


		// Define our AngularJS application module.
		var demo = angular.module( "Demo", [] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 500. This time, however, we're going
		// to compile and transclude the element - but only on this
		// directive - the other three remain exactly the same.
		demo.directive(
			"bnOne",
			function() {


				// I compile this element and return the linking
				// function to be used during the linking phase.
				function compile( element, attributes, transclude ) {

					// Store the transclude method on the link (just
					// to make it easier to read this code - this way
					// the link defintion doesn't have to be nested
					// inside the compile function).
					link.transclude = transclude;

					// Return our linking function.
					return( link );

				}


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnOne";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					// At this point, the "element" is the HTML
					// comment node that holds the base for the
					// transcluded element template. Now, we need to
					// include / inject the cloned template element
					// AFTER the comment node.
					//
					// NOTE: We're NOT using $scope.$new() in this
					// case since there's no need for this directive
					// to create a new scope for this clone.
					link.transclude(
						$scope,
						function( clone ) {

							element.after( clone );

						}
					);

					console.log( "bnOne ( priority: 500 + compile )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					compile: compile,
					controller: Controller,
					// link: No link function - compile() returns one.
					priority: 500,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A",
					transclude: "element"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 500.
		demo.directive(
			"bnTwo",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnTwo";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnTwo ( priority: 500 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 500,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 400.
		demo.directive(
			"bnThree",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnThree";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnThree ( priority: 400 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 400,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I execute at priority 400.
		demo.directive(
			"bnFour",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnFour";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnFour ( priority: 400 )" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the given
				// element.
				return({
					controller: Controller,
					link: link,
					priority: 400,
					require: [ "?bnOne", "?bnTwo", "?bnThree", "?bnFour" ],
					restrict: "A"
				});

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I am the child directive
		demo.directive(
			"bnChild",
			function() {


				// I am the controller for this directive.
				function Controller( $scope, $element, $attrs ) {

					this.id = "bnChild";

				}


				// I bind the $scope to the DOM behaviors.
				function link( $scope, element, attributes, controllers ) {

					console.log( "bnChild" );
					console.log( "----", controllers[ 0 ] );
					console.log( "----", controllers[ 1 ] );
					console.log( "----", controllers[ 2 ] );
					console.log( "----", controllers[ 3 ] );

				}


				// Return the directive confirugation. Notice that
				// this directive is (optionally) asking for each
				// controller of the four directives on the PARENT
				// element.
				return({
					controller: Controller,
					link: link,
					require: [ "?^bnOne", "?^bnTwo", "?^bnThree", "?^bnFour" ],
					restrict: "A"
				});

			}
		);


	</script>

</body>
</html>

Because we are transcluding the "element", we have to inject it back into the DOM during the bnOne link() phase. Now, before you go and think to yourself that I'm crazy to do it this way, realize that this is how the ngSwitchWhen, ngRepeat, and uiIf directives work.

This time, when we run the above code, we get the following console output:

AngularJS directive controllers being injected into other directives, after a compile() call.

As you can see, only bnOne and bnTwo could require the directive controllers for bnOne and bnTwo. The other three directives - bnThree, bnFour, bnChild - could only require the directive controllers for directives whose priority put their execution after the compile() call.

At this point, I don't fully understand what is going on. I know that the ability for one directive to see a parent directive's controller has nothing to do with DOM timing. In my Master-Slave example, a slave directive could always see the Master directive's Controller, no matter when the slave was added to the DOM. As such, I have a hunch that this has more to do with the separation between the compile and link phases.

Since bnOne has the potential to change the DOM, I can understand why bnOne and bnTwo couldn't see bnThree or bnFour; after all, bnThree and bnFour execute at a lower priority and may not even exist (in a different scenario) post-compile. But, that wouldn't explain why bnThree, bnFour, and bnChild couldn't see the bnOne and bnTwo directive controllers?

And, even if bnThree and bnFour were somehow messed up by the compile and priority settings, I still can't understand why bnChild couldn't see all of the parent directive controllers.

If anyone has any deeper understanding of how the compile() method is affecting the linking phase of the various directives, I would be hugely grateful. I have a very specific use-case in mind for a child directive being able to communicate with the controller of a parent directive that needs to compile().

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

I'm using ui-router's $stateProvider on my site and I am having trouble getting directives to work anywhere other than on my homepage. For example, I am using ui-date for a datepicker solution. According to the ui-date website I just place

<input ui-date>

on my site to get a datepicker. This works as expected on my homepage, but if I place the exact same tag on a template that is loaded via a $stateProvider URL change then it no longer works. What am I supposed to do to get this to work?

1 Comments

Hi Ben, just wondering if you've come across how to unit test directive controllers? I've got unit testing of directives down to a tee and I can use $provide to inject mock services and check functions are all being called correctly etc using jasmines expect(someFunction).toHaveBeenCalled() on a spy that I've created. Unfortunately, as of yet, the only way of testing directive controllers is to compile the element they belong to as well and then watch the interaction. I've tried using $provide but it doesn't seem to work and I just wondered if you'd come across a solution as some of my directives with controllers are higher level ones and it means practically compiling the whole app just to get directive controllers tested rather than breaking it up into units.

Thanks in advance!

Kind regards,

Dave

2 Comments

Ok so not sure what version of Angular was out when you posted this, but the compile transclude function has been depreciated. Here is a jsfiddle showing how to do this with the link funciton's transcludeFn parameter http://jsfiddle.net/joshkurz/Hn8d9/2/ which is the 5th option for the link function. bs2 3 and 4' controllers are still undefined for bsOne's link funciton but I believe you are right in saying that they do not exist yet, but the controller for bsOne is visible to all now.

2 Comments

@Ben,

I am building a tree view with directives, and am having some issues in knowing when the tree view has completed rendering....

Basically it has nested children and what not, however i want to do a (select) on one of the noes AFTER the tree view is built.

I am able to do a selection on the node while the tree is being built with an if statement, but am unsure of how to make the selection after it has been built.

Is there a way to find out when the template is fully built? I am using a link: function and compile.

This is the original tree view:
http://ngmodules.org/modules/angular.treeview

Merci!

1 Comments

Great organization technique -- I really like ctrl and link functions inside of the directive and then using them in the return, looks clean, seems easy to maintain.

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