Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with:

Looking At $compile() And MaxPriority In AngularJS

By Ben Nadel on

The other day, I was looking through the AngularJS documentation for funzies (thug-life!) and I happened to notice that the $compile() service took some optional arguments, one of which was called "maxPriority". I immediately thought of being able to defer the linking of directives until a later time; but I couldn't get anything to work. Thankfully, after some guidance from Michael Bromley and Stephen Barker, I think I am starting to understand how this invocation of $compile() can be used.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When you define your AngularJS directives, you can provide both a compile function and a link function. The compile function can be used to change the local DOM (Document Object Model) subtree before it is compiled and linked. However, not all changes actually get linked. If you alter the child-content of the current element, those changes will be automatically be compiled and linked by AngularJS; but, if you alter the attributes of the current element, those changes will not be automatically compiled since AngularJS has already "collected" the directives on the current element.

As such, if you want to add attribute-directives to the current element, you have to explicitly manage the compilation and linking process on the current element. And, this is where the "maxPriority" argument of $compile() comes into play. If we know that our directive is going to add new attribute-directives to the current element, it has to add them at a lower priority; and, it's going to have to explicitly compile the directives at the lower priority. And, that's what "maxPriority" does - it compiles an element, but only includes attributes below the given max-priority.

Once we start explicitly calling $compile() on the current element, however, we have to start using the "terminal" property on our directive. If we don't, then directives at lower priorities, and the child content, will end up getting compiled and linked twice, which we obviously don't want.

In the demo below, I also wanted to use the "isolate" scope. This ended up causing further complications because we are explicitly compiling and linking our content. And, in the context of the isolate scope, the child content was incorrectly being linked to the isolate scope, not to the parent scope chain. As such, we had to start using the "transclude" property as well.

Since the transclude() function, passed to any directive that uses transclusion, is automatically bound the parent scope, it allows us to properly link child content that is inside an explicitly-compiled context. This means that our explicit call to $compile() - and the returned link function - act only on the local element, and not on the child content, which is linked with the given transclude() function.

Obviously, this is some complicated, heady stuff! And, what's more, I still don't know what the "transclude" argument in the $compile() call is for, which is why we are leaving it as null in the following code. And, since I'm still letting this is all sink in, I'll defer any further explanation to the code-comments below and the video above.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Looking At $compile() And MaxPriority In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Looking At $compile() And MaxPriority In AngularJS
  • </h1>
  •  
  • <!--
  • The bn-friend directive will dyanmically add other AngularJS directives to
  • this element and then compile them.
  • -->
  • <div bn-friend="friend" bn-log="Outer div.">
  •  
  • My
  • <span ng-show="friend.isBest" bn-log="Inner span.">best</span>
  • friend is {{ friend.name }}
  •  
  • </div>
  •  
  • <p>
  • <em>NOTE: <a href="index2.htm">Run version without isolate-scope</a></em>
  • </p>
  •  
  •  
  • <!-- 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.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • $scope.friend = {
  • name: "Kim",
  • isBest: true
  • };
  •  
  • console.log( "Demo scope:", $scope );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I log to the console during the linking function so we can see when things
  • // are being executed (and how many times they are being executed).
  • app.directive(
  • "bnLog",
  • function() {
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes ) {
  •  
  • var now = ( new Date() ).getTime();
  •  
  • console.log( "Log [", attributes.bnLog, "]", now );
  •  
  • // Logging scope so you can see how scope is affected.
  • console.log( $scope );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • priority: 1010,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I dynamically add new directives to the current element.
  • app.directive(
  • "bnFriend",
  • function( $compile ) {
  •  
  • // I augment the template element DOM structure before linking.
  • function compile( tElement, tAttributes ) {
  •  
  • // Add a static HTML attribute.
  • tElement.attr( "class", "friend" );
  •  
  • // Add the ng-class directive. Notice that the item reference here
  • // is the ISOLATE scope reference.
  • tElement.attr( "ng-class", "{ best: isolateFriend.isBest }" );
  •  
  •  
  • // At this point, the ng-class directive WILL NOT be automatically
  • // compiled. As such, we need to explicitly compile the element,
  • // starting at the max-priority of the current directive. This will
  • // compile all directives on this element, lower than 1500, AND all
  • // the content of the element (regardless of priority).
  • // --
  • // NOTE: This is why we need to use TERMINAL in our directive
  • // configuration - if we didn't then lower-priority directives on the
  • // element would actually be compiled and linked twice.
  • var sublink = $compile( tElement, null, 1500 );
  •  
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes, _, transclude ) {
  •  
  • // Because we are using the ISOLATE scope, in this case, we have
  • // to transclude the content. If we don't do this, then the call
  • // to $compile() above and sublink() below will end up linking
  • // the element CONTENT to the ISOLATE scope, which will break
  • // our references. So, instead, what we have to do is allow the
  • // content to be transcluded and linked to the outer scope
  • // (outside of our directive).
  • transclude(
  • function( content ) {
  •  
  • element.append( content );
  •  
  • }
  • );
  •  
  • // Link the compiled directives that we dynamically added to the
  • // current element. This will also link any directives that were
  • // already on the element, but were at a lower priority.
  • // --
  • // NOTE: We probably we want to do this after the transclude()
  • // since a directive is supposed to be able to rely on the DOM
  • // of its child content.
  • sublink( $scope );
  •  
  • }
  •  
  •  
  • return( link );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • // --
  • // NOTE: There a bunch of little interactions going on here. For
  • // starters, we have to TERMINAL in our configuration otherwise lower-
  • // priority directives on the same element would compile twice (due to
  • // our explicit call to $compeil()). Also, since we are using the ISOLATE
  • // scope (for this demo - not required to use maxPriority), we also have
  • // to use TRANSCLUDE; if we didn't, then our the elements child content
  • // would be inappropriately linked to the ISOLATE scope, not to the
  • // "parent" scope in the scope chain. If we didn't use ISOLATE scope, we
  • // would NOT have to the use TRANSCLUDE.
  • return({
  • compile: compile,
  • priority: 1500,
  • restrict: "A",
  • scope: {
  • isolateFriend: "=bnFriend"
  • },
  • terminal: true,
  • transclude: true
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

After further thought (and some stream of consciousness in the video), I've come the conclusion that you should probably never use the isolate scope if you are going to being using $compile() with the "maxPriority" argument. The reason for this is that you'll end up with higher-priority directives getting linked in the parent scope chain and lower-priority directives getting linked in the isolate scope chain; which is most definitely not what you would want (or even intended).

To remove the "noise", I've taken the above demo and factored-out all of the isolate scope functionality. It should make the $compile() feature a bit easier to see:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Looking At $compile() And MaxPriority In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Looking At $compile() And MaxPriority In AngularJS
  • </h1>
  •  
  • <!--
  • The bn-friend directive will dyanmically add other AngularJS directives to
  • this element and then compile them.
  • -->
  • <div bn-friend="friend" bn-log="Outer div.">
  •  
  • My
  • <span ng-show="friend.isBest" bn-log="Inner span.">best</span>
  • friend is {{ friend.name }}
  •  
  • </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.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • $scope.friend = {
  • name: "Kim",
  • isBest: true
  • };
  •  
  • console.log( "Demo scope:", $scope );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I log to the console during the linking function so we can see when things
  • // are being executed (and how many times they are being executed).
  • app.directive(
  • "bnLog",
  • function() {
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes ) {
  •  
  • var now = ( new Date() ).getTime();
  •  
  • console.log( "Log [", attributes.bnLog, "]", now );
  •  
  • // Logging scope so you can see how scope is affected.
  • console.log( $scope );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • priority: 1010,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I dynamically add new directives to the current element.
  • app.directive(
  • "bnFriend",
  • function( $compile ) {
  •  
  • // I augment the template element DOM structure before linking.
  • function compile( tElement, tAttributes ) {
  •  
  • // Add a static HTML attribute.
  • tElement.attr( "class", "friend" );
  •  
  • // Add the ng-class directive. Notice that we have to pass-through
  • // the reference to the actual friend since we don't know what it
  • // will actually be called.
  • tElement.attr(
  • "ng-class",
  • ( "{ best: " + tAttributes.bnFriend + ".isBest }" )
  • );
  •  
  •  
  • // At this point, the ng-class directive WILL NOT be automatically
  • // compiled. As such, we need to explicitly compile the element,
  • // starting at the max-priority of the current directive. This will
  • // compile all directives on this element, lower than 1500, AND all
  • // the content of the element (regardless of priority).
  • // --
  • // NOTE: This is why we need to use TERMINAL in our directive
  • // configuration - if we didn't then lower-priority directives on the
  • // element would actually be compiled and linked twice.
  • var sublink = $compile( tElement, null, 1500 );
  •  
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes ) {
  •  
  • // Link the compiled directives that we dynamically added to the
  • // current element. This will also link any directives that were
  • // already on the element, but were at a lower priority.
  • // --
  • // NOTE: We probably we want to do this after the transclude()
  • // since a directive is supposed to be able to rely on the DOM
  • // of its child content.
  • sublink( $scope );
  •  
  • }
  •  
  •  
  • return( link );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • // --
  • // NOTE: We have to TERMINAL in our configuration otherwise lower-
  • // priority directives on the same element would compile twice (due to
  • // our explicit call to $compeil()).
  • return({
  • compile: compile,
  • priority: 1500,
  • restrict: "A",
  • terminal: true
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, without the isolate scope, the entire linking process becomes more straight-forward.

This is some interesting stuff. I don't have too much real-world experience with AngularJS directives that alter the DOM or transclude content. But, the more I learn about it, the more I'll [hopefully] find use-cases for it.




Reader Comments

I have a directive which essentially creates a dropdown box (lets call it nameDropdown) . It's a common HTML element which will be used everywhere in my project. I'm trying to dynamically add a directive called "select-required" onto it.

Where I define it on my HTML page ( <name-dropdown is-required="true"> )

If is-required = true I want to add the directive to the element. However, it gets into an infinite loop. My code in the directive is as follows:

link : function(scope, element, attrs) {
scope.id = attrs.id;
scope.element = element.find("select");
if (scope.isRequired === true) {
scope.element.attr("select-required", "");
// if i put compile here it runs an infinite loop??
}
}

I have to use an isolated scope, its completely and utterly needed. And I have to have a templateURL rather than the template string in the file.

Any ideas at all?

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.