Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Guust Nieuwenhuis
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Guust Nieuwenhuis@Lagaffe )

Directive Architecture, Template URLs, And Linking Order In AngularJS

By Ben Nadel 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.




Reader 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.

Reply to this Comment

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?

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.