Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Phillips
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Phillips@cfchris )

Injecting Sibling Nodes During The Compilation Phase Of AngularJS Directives

By Ben Nadel on

Typically, the compilation phase of an AngularJS directive deals with altering the contents of the current DOM (Document Object Model) node. However, in edge-cases, you may need to replace the current DOM node or inject a new sibling node. Sibling nodes are funny - sometimes injecting them is safe; other times, it can throw your page into an infinite loop. The key to safety is understand how AngularJS walks the DOM tree when it's compiling the markup.


 
 
 

 
 
 
 
 

If you drill down into the AngularJS source code, you will eventually find a function called compileNodes(). In that function, AngularJS performs a depth-first traversal of the DOM tree, compiling directives and aggregating linking functions. The mechanics of this loop, and specifically, its recursive nature, are very interesting. AngularJS loops over the "childNodes" collection using an index loop.

If it's been a while since you've accessed the DOM without jQuery (or jqLite), you may not remember that the childNodes collection is a "live" collection. This means that if you alter the DOM, existing referencing to this collection will also change. This is very different than a jQuery (or jqLite) collection, which is static once it has been created.

Furthermore, the actual for-loop, of the DOM traversal, checks the length of this collection on each iteration:

  • for ( var i = 0 ; i < nodeList.length ; i++ ) {
  • // Compile nodeList[ i ] ....
  • }

Altogether, this means that if your directive injects a sibling DOM node, it will actively affect the current for-loop in which it is being compiled. If it injects a node after itself, this is safe - the newly injected node is automatically picked up in the next iteration of the for-loop. If, however, the sibling node is injected before the current node, this effectively bumps the current node into the next iteration of the for-loop, which causes it be compiled again, and again, and again. Forever.

This blog post is mostly context for the above video; but, here is the code that I an demonstrating in that video:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Injecting Sibling Nodes During The Compile Phase Of AngularJS Directives
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Injecting Sibling Nodes During The Compile Phase Of AngularJS Directives
  • </h1>
  •  
  • <ul>
  • <li bn-primary>
  • Sarah -- {{ message }}
  • </li>
  • <li bn-primary>
  • Tricia -- {{ message }}
  • </li>
  • <li bn-primary>
  • Kim -- {{ message }}
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="./angular-1.3.6.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.message = "testing interpolation";
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I alter my sibling DOM tree to see how it affects the compilation process
  • // as AngularJS walks the DOM, applying directives.
  • app.directive(
  • "bnPrimary",
  • function() {
  •  
  • // Return the directive configuration.
  • return({
  • compile: compile,
  • restrict: "A"
  • });
  •  
  •  
  • // I compile the current element, and, in this case, alter the sibling
  • // DOM nodes for experimentation.
  • function compile( tElement, tAttributes ) {
  •  
  • // Get the current HTML for the node. This will bring in any markup
  • // as well as any {{ interpolation }} syntax.
  • var html = tElement.html();
  •  
  • // Now, we're going to alter the sibilng node list.
  • tElement.after( "<li>" + html + "</li>" );
  •  
  • // Now, we're going to alter the sibilng node list.
  • // --
  • // CAUTION: This will cause AngularJS to compile "forever".
  • // tElement.before( "<li>" + html + "</li>" );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Altering the sibling node-list in the compilation phase of an AngularJS directive is definitely an edge-case. But, depending on what you need to do, it can be done safely.




Reader Comments

@All,

An interesting byproduct of this behavior might be related to the fact that jqLite implements a .after() method, but *not* a .before() method. Could be totally coincidental; or, this could be because injecting things after the currently element is always safer?

Reply to this Comment

So I understand talking about this issue, but I think you should still slap lots of warnings and avoidance messages all over the place. This is generally a worst-case scenario solution that can be avoided by using css psuedo-elements (::before and ::after) or using templates. In addition, injecting into children tends to also be even safer as the compiling is going from top to bottom.

You also didn't mention things like when you don't want something to be compiled or managed by angular, such as when you perform injecting in the linking phases (which is just bad in general though).

The bottom line, I would hope to inform people of WHEN injecting DOM (this way too) is even worthwhile since for 99.999% of the time it's unnecessary. I would also point out that this buggy behavior is the reason many beginners experience confusion as to why their app suddenly breaks or things don't work when they try to simply drop-in jquery plugins and expect them to behave normally.

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.