Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andy Matthews
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Andy Matthews@commadelimited )

Compiling Transcluded Content in AngularJS Directives

By Ben Nadel on

When you use the "transclude" option, in an AnglarJS directive, the transcluded content is automatically removed from its container, compiled, and made available through a linking function. This process keeps the transcluded content completely black-boxed. However, if we need to explicitly compile the transcluded content (so as to augment the pre-linked document object model), we can leverage Angular's ability to bind a single directive to multiple priorities on the same element.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When a directive is executed on an element, it's executed at a certain priority (defaults to zero). So, if you want to modify the DOM (Document Object Model) of your transcluded content before it's linked, you can't do this at the "transclusion priority." Instead, you have to do this before the transclusion priority; that is, you have to modify the pre-transcluded DOM at a higher priority.

That's a mouthful of poorly-articulated thoughts. Probably, this is just easier to see in code. In the following demo, I have a directive on a UL element. For the sake of the demo, the UL element is going to transclude the LI element and then simply re-append it. But, the key part of this exploration is the fact that the UL directive needs to add a CSS class to the LI template before the ngRepeat directive executes.

The UL won't have access to the LI content in either the compile() or the link() function of the transclude directive. As such, it will need to bind to another compile function, at a higher priority, so that it can add the CSS class prior to the transclusion:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Compiling Transcluded Content in AngularJS Directives
  • </title>
  •  
  • <style type="text/css">
  •  
  • ul.container {
  • border: 2px solid #CC0000 ;
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 10px 10px 6px 10px ;
  • }
  •  
  • li.item {
  • border: 1px dotted #CC0000 ;
  • margin: 0px 0px 4px 0px ;
  • padding: 10px 10px 10px 10px ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Compiling Transcluded Content in AngularJS Directives
  • </h1>
  •  
  • <ul bn-thing>
  • <li ng-repeat="friend in friends track by friend.id">
  •  
  • {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- 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.3.6.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.friends = [
  • {
  • id: 1,
  • name: "Kim"
  • },
  • {
  • id: 2,
  • name: "Sarah"
  • },
  • {
  • id: 3,
  • name: "Tricia"
  • }
  • ];
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I run the directive at a priority that executes pre-transclusion. This gives
  • // us an opportunity to alter / compile the content before it is transcluded by
  • // the lower-priority hook for the directive.
  • app.directive(
  • "bnThing",
  • function() {
  •  
  • // Return the directive configuration. Notice that we are running at
  • // priority 1500.1, which is a higher priority than the next directive
  • // binding.
  • return({
  • compile: compile,
  • priority: 1500.1,
  • restrict: "A"
  • });
  •  
  •  
  • // I compile the pre-transcluded content.
  • function compile( tElement, tAttributes ) {
  •  
  • tElement.children()
  • .addClass( "item" )
  • ;
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // I transclude the directive content BACK INTO the current container (to
  • // demonstrate the concept). At this point, we no longer have access to the
  • // transcluded content until it is cloned, which is too late to augment it.
  • app.directive(
  • "bnThing",
  • function() {
  •  
  • // Return the directive configuration. Notice that we are running at
  • // priority 1500, which is a lower priority than the previous directive
  • // binding (the one that augmented the transcluded content).
  • return({
  • compile: compile,
  • priority: 1500,
  • restrict: "A",
  • transclude: true
  • });
  •  
  •  
  • // I compile the directive. Since we are transcluding the entire element,
  • // we only have access to the "anchor comment" in this context.
  • function compile( tElement, tAttributes ) {
  •  
  • console.log( "Container at compile (html):", tElement.html() );
  •  
  • tElement.addClass( "container" );
  •  
  • return( link );
  •  
  • }
  •  
  •  
  • // I link the directive. Since we can only access the content via the
  • // transclusion function, at this point, the content has already been
  • // compiled by the time we get the clone.
  • function link( scope, element, attributes, _c, transclude ) {
  •  
  • console.log( "Container at link (html):", element.html() );
  •  
  • // Clone and inject the transcluded content.
  • transclude(
  • function injectLinkedClone( clone ) {
  •  
  • element.append( clone );
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we're logging out the root element in the compile and link functions of the transclude directive. And, when we run this directive, we get the following console output:

Container at compile (html): (an empty string)
Container at link (html): (an empty string)

As you can see, when a directive uses "transclude", it can't access the transcluded content until it actually clones it. And, at that point, it's too late to alter the templates. However, since our directive was also bound to a compile function at a higher priority, it was able to add the CSS class, "item". And, as such, the ngRepeat included that class in each clone:


 
 
 

 
 Compile transcluded content in AngularJS directive, before content is transcluded. 
 
 
 

This is probably more of an edge case. And, you could always get around this by swapping the "transclude" property out with an explicit call to the $compile() function. But, I think it's good to know what content you have access to during the native transclusion lifecycle. And, what options you have.




Reader Comments

Well man, this article came in handy for me, that's exactly what I've been looking for for hours. Thank you so much, makes my day !

Reply to this Comment

Thanks a lot this was very educational and I used it in my project. Kinda gives me lisp macros vibes.

html is data :)

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.