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

Directive Architecture, Template URLs, And Linking Order In AngularJS

By on

When it comes to directive compiling, linking, and general timing in AngularJS, it probably feels like I'm beating a dead horse. But, the reason that I keep revisiting the topic is because the directive workflow is a complex multi-faceted process that keeps revealing new quirks and edge-cases. Today, I want to take a look at how link timing works in directives that use a templateUrl in different rendering contexts.

Run this demo in my JavaScript Demos project on GitHub.

When AngularJS is compiling HTML and comes across a directive that uses the templateUrl setting, it processes the templateUrl asynchronously using the $templateRequest() service. Instead of blocking the compile process, AngularJS queues the sub-tree compiling function and continues to compile and link the rest of the HTML. From the documentation on templateUrl:

Because template loading is asynchronous the compiler will suspend compilation of directives on that element for later when the template has been resolved. In the meantime it will continue to compile and link sibling and parent elements as though this element had not contained any directives.

The compiler does not suspend the entire compilation to wait for templates to be loaded because this would result in the whole app "stalling" until all templates are loaded asynchronously - even in the case when only one deeply nested directive has templateUrl.

Template loading is asynchronous even if the template has been preloaded into the $templateCache

While directives usually link from the bottom-up, the use and context of templateUrls will deviate from this standard workflow. To see this in action, I've put together a demo that contains a number of nested directives that use templateUrl. Some of them are rendered immediately; some of them are rendering based on view-model values.

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

	<title>
		Directive Architecture, Template URLs, And Linking Order In AngularJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		Directive Architecture, Template URLs, And Linking Order In AngularJS
	</h1>


	<!-- This set of directives is directly in the initial HTML, nothing fancy. -->
	<div a-outer>

		A-outer directive. <em>This directive is defined directly in the HTML</em>

		<div a-inner>

			A-inner directive.

		</div>

	</div>


	<!--
		This set of directives is available in the initial HTML, but the templates are
		provided using ngTemplate Script tags. Since templateUrl values are compiled
		asynchronously, the parent directive will actually be linked first, while the
		template is being processed.
	-->
	<div b-outer></div>

	<script type="text/ng-template" id="b-outer.htm">

		B-outer directive. <em>This directive is defined using inlined ngTemplate</em>

		<div b-inner></div>

	</script>

	<script type="text/ng-template" id="b-inner.htm">

		B-inner directive.

	</script>


	<!--
		This set of directives is available in the initial HTML, but the templates are
		provided using ngTemplate Script tags and the outer directive is deferred
		using ngIf. However, since the c-outer directive is in the initial HTML, it is
		compiled and the templateUrl is processed. As such, by the then ng-if brings
		the directive into existence, the templateUrl does not need to compiled
		asynchronously and it can be processed as though it was inline, allowing the
		directives to link from the bottom-up.
	-->
	<p>
		<a ng-click="toggleC()">Toggle C</a>
	</p>

	<div ng-if="isShowingC" c-outer></div>

	<script type="text/ng-template" id="c-outer.htm">

		C-outer directive. <em>This directive is deferred using ngIf</em>

		<div c-inner></div>

	</script>

	<script type="text/ng-template" id="c-inner.htm">

		C-inner directive.

	</script>


	<!--
		This set of directives is NOT available in the initial HTML - it's not available
		until the ngInclude directive is linked. This means that the outer directive,
		d-outer, isn't compiled until the ng-if renders it. As such, we are back in a
		situation where the templateUrls need to be processed asynchronously which means
		that the outer directive will link before the inner directive.
	-->
	<p>
		<a ng-click="toggleD()">Toggle D</a>
	</p>

	<div ng-if="isShowingD" ng-include=" 'd-parent.htm' "></div>

	<script type="text/ng-template" id="d-parent.htm">

		D-outer parent (ng-include).

		<div d-outer></div>

	</script>

	<script type="text/ng-template" id="d-outer.htm">

		D-outer directive. <em>This directive is deferred using ngIf / ngInclude parent</em>

		<div d-inner></div>

	</script>

	<script type="text/ng-template" id="d-inner.htm">

		D-inner directive.

	</script>


	<!--
		This set of directives is available in the initial HTML, but the directive
		templates are provided using remote URLs. The rendering of the directive is
		deferred using ngIf; however, since the e-outer directive is available in the
		initial HTML, it will be compiled. The templateUrls will then be retrieved and
		compiled asynchronously; however, since the directive isn't actually rendered
		until the ng-if transcludes it, it will be able to link from the bottom up.
	-->
	<p>
		<a ng-click="toggleE()">Toggle E</a>
	</p>

	<div ng-if="isShowingE" e-outer></div>



	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.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.isShowingC = false;
				$scope.isShowingD = false;
				$scope.isShowingE = false;


				// ---
				// PUBLIC METHODS.
				// ---


				$scope.toggleC = function() {

					console.info( "Toggling C" );

					$scope.isShowingC = ! $scope.isShowingC;

				};

				$scope.toggleD = function() {

					console.info( "Toggling D" );

					$scope.isShowingD = ! $scope.isShowingD;

				};

				$scope.toggleE = function() {

					console.info( "Toggling E" );

					$scope.isShowingE = ! $scope.isShowingE;

				};

			}
		);


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


		app.directive(
			"body",
			function() {

				return( link );

				function link( scope, element, attributes ) {

					console.log( "Body linked" );

				}

			}
		);


		app.directive(
			"aOuter",
			function() {

				return( link );

				function link( scope, element, attributes ) {

					console.log( "A-Outer linked" );

				}

			}
		);


		app.directive(
			"aInner",
			function() {

				return( link );

				function link( scope, element, attributes ) {

					console.log( "A-Inner linked" );

				}

			}
		);


		app.directive(
			"bOuter",
			function() {

				return({
					link: link,
					templateUrl: "b-outer.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "B-Outer linked" );

				}

			}
		);


		app.directive(
			"bInner",
			function() {

				return({
					link: link,
					templateUrl: "b-inner.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "B-Inner linked" );

				}

			}
		);


		app.directive(
			"cOuter",
			function() {

				return({
					link: link,
					templateUrl: "c-outer.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "C-Outer linked" );

				}

			}
		);


		app.directive(
			"cInner",
			function() {

				return({
					link: link,
					templateUrl: "c-inner.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "C-Inner linked" );

				}

			}
		);


		app.directive(
			"dOuter",
			function() {

				return({
					link: link,
					templateUrl: "d-outer.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "D-Outer linked" );

				}

			}
		);


		app.directive(
			"dInner",
			function() {

				return({
					link: link,
					templateUrl: "d-inner.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "D-Inner linked" );

				}

			}
		);


		app.directive(
			"eOuter",
			function() {

				return({
					link: link,
					templateUrl: "e-outer.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "E-Outer linked" );

				}

			}
		);


		app.directive(
			"eInner",
			function() {

				return({
					link: link,
					templateUrl: "e-inner.htm"
				});

				function link( scope, element, attributes ) {

					console.log( "E-Inner linked" );

				}

			}
		);

	</script>

</body>
</html>

The key to understanding this code is in seeing when the outer directive is available to the compile process. If the compile process can access the templateUrl before the directive needs to be rendered, things can be linked from the bottom-up. But, if the directive needs to be rendered immediately, the compile process will not have time to process the templateUrl and will, therefore, have to link the parent before it links the nested directive.

In the above demo, if I toggle the three deferred directives, we end up getting the following output:

templateUrl compile and link timing in AngularJS directives.

Notice that directives B and D link from the top-down, not the bottom-up. This is because the need to render coincides with the compilation of the parent directive, which doesn't give the templateUrl enough time to be pre-processed.

The key take-away here is that if you are building a directive the only think you can possible take for granted is the structure and timing of the directive you are creating. The moment you move down into nested directives that you don't control, all of your assumptions go out the window. While this is less meaningful for your own application, it is incredibly important if you are creating 3rd-party directives for distribution. In that case, the assumptions that you can make in your distributed directive must necessarily be more conservative.

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

Reader Comments

15,688 Comments

@Karthik,

After the page is loaded, it should replace the monochrome source code with color-coded. But, its a call to the GitHub API, so it may be a little slow from time to time. Try refreshing the page and waiting a second.

2 Comments

Ben, this is great. I came to this post because I was struggling with the whole initialising/re-initialising a directive/component. Stackoverflow question here: http://stackoverflow.com/questions/34682739/initialising-and-re-initialising-a-component-in-angularjs

I found that when using the first pattern that I mentioned there (1. Pass update function), if I switched to templateUrl from template on the child (<clock> in my example), not even a $timeout saves me (unless you set it to a number of ms long enough to fire after the templateUrl is fetched), because (I think) the <clock> is waiting for the template to return, and meanwhile the parent controller is already calling the <clock> init function which doesn't exist because of the template async call.

What's your take on my Stackoverflow question? Have you ever encountered this problem?

2 Comments

Thank you a lot for this explanation.

We ran into this problem when had decided to replace our templateUrl with template and just load templates with appropriate loader. And after that something went wrong with initialize order.

2 Comments

Thank you a lot for this explanation.

We ran into this problem when had decided to replace our templateUrl with template and just load templates with appropriate loader. And after that something went wrong with initialize order.

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