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

Posted February 9, 2013 at 6:41 PM by Ben Nadel

Tags: Javascript / DHTML

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().




Reader Comments

Mar 28, 2013 at 5:16 AM // reply »
1 Comments

Very Nice ^ ^


Apr 2, 2013 at 4:59 PM // reply »
11,246 Comments

@Tengfei,

Thank you!


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 24, 2013 at 5:39 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Adam Oops! My mistake! I hadn't gotten that far in my testing - I'm still baby stepping my way through the process. ... read »
May 24, 2013 at 5:13 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
Hi Jason, Thanks for checking up on that, but I still stand firm on my position. :) There are actually two listLast()'s in use, and you're right that the one using a space as a delimiter is fine. ... read »
May 24, 2013 at 4:45 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Ben I have been lurking your site for quite some time, and haven't stepped up to comment until today. Thanks for all the great info - keep it up! @Adam I believe you are mistaken... as the commen ... read »
May 24, 2013 at 11:21 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@WebManWalking, Ha ha, let's us never speak of justifying "##" notation again :P ... read »
May 24, 2013 at 11:18 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Ah, so it was indeed how I vaguely remembered it to be: A direct assignment value = users.id[ i ] causes value to retain the sticky datatype of the query column. Although unnecessary in ... read »
May 24, 2013 at 9:11 AM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Brandon, Hi, No, I haven't been able to do that. I have just kept it as it is. ... read »
May 23, 2013 at 9:52 PM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Muhmmadibn Did you figure out a solution to launching PDFs? I am running into the same issues myself. There is no way to close the PDF or go back once you launch it. Thanks in advance! ... read »
May 23, 2013 at 6:06 PM
The Girl Who Broke My Heart, And Made Me A Better Person
Good day,ladies and gentle men, my name is Dr AMADI the great spell caster in Africa, i have help so many people for different kind of problems,who say there is no solution to problems on earth, that ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools