Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch

Looking At $compile() And MaxPriority In AngularJS

By
Published in Comments (9)

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.

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

Reader Comments

1 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?

4 Comments

Hi Ben,

I faced the exact same situation today, minus the isolated scope headache.

Your post is crystal clear and helped me to figure where I should add the directive attributes (controller ? prelink ? compile ?). Now I got it working as I wanted to.

Thanks a lot !

2 Comments

Thanks Ben. Very helpful article!

Is it possible to use a directive that does this within an ng-repeat? Using guidance from this article, I've created a directive that applies other directives to my element, and it works great by itself. When I put it into an ng-repeat, the bindings quit working. If I inspect my element, everything looks correct, but it's not binding to my model values.

4 Comments

Hi Ben,

I've just noticed a bug while using this bnFriend inside a ngRepeat directive. You shouldn't use $compile in compile function because you are compiling the original dom template. By doing so, you are linking the original dom element against the scope in the link, which is fine in an non ngRepeat environment. But when you are inside a ngRepeat, the link function will get called with a clone of the original dom element. So your cloned element will never get linked.

I got it working by doing the compile and linking in the prelink function.

2 Comments

Thank you Ben. And Anthony, because I had also issues with the compiling inside first.
But I still have one big problem.
I use this kind of directive on a node, where I have a second directive like this:

app.directive('bmUndoButton', function () {
'use strict';
return {
restrict: 'A',
transclude: true,
replace: true,
template: '<div><i data-ng-transclude></div>'
};
});

This gives me an error "maximum stack size exceeded" because of endless recursion. Do you have an idea to fix that?

2 Comments

oh no sorry. its not on the same node, but i have a directive like that:

app.directive('bmUndoButton', function () {
'use strict';
return {
restrict: 'A',
transclude: true,
replace: true,
template: '<div bn-friend><i data-ng-transclude></div>'
};
});

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