Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with:

Compound Transclusion Prevented In AngularJS 1.2

By Ben Nadel on

One of the huge changes in AngularJS 1.2 is the addition of an Animation module that allows DOM (Document Object Model) elements to be animated into and out of existence using CSS3 (with fallbacks to JavaScript). I don't feel very strongly about animations; in fact, I think the "track-by" update is the coolest part of AngularJS 1.2. That said, I'm sad to see Animations added at the expense of compound transclusion on a single DOM node.

View this demo in my JavaScript-Demos project on GitHub.

When I say "compound transclusion," what I mean is the need for multiple directives to perform transclusion on a single DOM node. While this might sound like a super advanced topic, it's doesn't have to be; using ngSwitchWhen and ngInclude on a single element is a very common and mundane example of compound translcusion.

Of course, things can get more advanced if you want. In my previous post on preloading data before executing ngInclude, I actually have three different directives - ngSwitchWhen, bnPreload, ngInclude - performing transclusion on the same element. Transclusion is super powerful; and, the ability to perform multiple transclusions on a single DOM node is a worthy endeavor.

That said, with AngularJS 1.2, compound transclusion is no longer available. With a few exceptions (which has a special work-arounds in the actual AngularJS code), compound transclusion will raise a $compile exception, example:

Multiple Directive Resource Contention. Multiple directives [ngSwitchWhen, ngInclude] asking for transclusion on...

To see this error in action, take a look at this super simple demo which does nothing more than attempt to render an ngInclude based on a switch statement (a very common scenario):

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Compound Transclusion Prevented In AngularJS 1.2
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Compound Transclusion Prevented In AngularJS 1.2
  • </h1>
  •  
  • <p>
  • Show:
  • <a ng-click="showSubview( 'one' )">One</a> or
  • <a ng-click="showSubview( 'two' )">Two</a>
  • </p>
  •  
  • <!-- Render the ngInclude based on the switch. -->
  • <div ng-switch="subview">
  • <div ng-switch-when="one" ng-include=" 'one.htm' "></div>
  • <div ng-switch-when="two" ng-include=" 'two.htm' "></div>
  • </div>
  •  
  • <p>
  • <a href="./index.htm">Breaking Version</a><br />
  • <a href="./working.htm">Working Version</a><br />
  • </p>
  •  
  •  
  • <!-- Template for ngInclude. -->
  • <script type="text/ng-template" id="one.htm">
  •  
  • <div>
  • Template One
  • </div>
  •  
  • </script>
  •  
  •  
  • <!-- Template for ngInclude. -->
  • <script type="text/ng-template" id="two.htm">
  •  
  • <div>
  • Template Two
  • </div>
  •  
  • </script>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.4.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 ) {
  •  
  • // I determine which subview to render.
  • $scope.subview = "one";
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I show the given subview.
  • $scope.showSubview = function( newSubview ) {
  •  
  • $scope.subview = newSubview;
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

While this worked in AngularJS 1.0.x, this now breaks in AngularJS 1.2.x with the error stated above ("multidir"). The cause - using ngSwitchWhen and ngInclude on the same DOM element.

To work around this issue, you have to arbitrarily nest DOM elements. In this case, we can add a Div[ngInclude] inside of the Div[ngSwitchWhen]:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Compound Transclusion Prevented In AngularJS 1.2
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Compound Transclusion Prevented In AngularJS 1.2
  • </h1>
  •  
  • <p>
  • Show:
  • <a ng-click="showSubview( 'one' )">One</a> or
  • <a ng-click="showSubview( 'two' )">Two</a>
  • </p>
  •  
  • <!-- Render the ngInclude based on the switch. -->
  • <div ng-switch="subview">
  • <div ng-switch-when="one">
  • <div ng-include=" 'one.htm' "></div>
  • </div>
  • <div ng-switch-when="two">
  • <div ng-include=" 'two.htm' "></div>
  • </div>
  • </div>
  •  
  • <p>
  • <a href="./index.htm">Breaking Version</a><br />
  • <a href="./working.htm">Working Version</a><br />
  • </p>
  •  
  •  
  • <!-- Template for ngInclude. -->
  • <script type="text/ng-template" id="one.htm">
  •  
  • <div>
  • Template One
  • </div>
  •  
  • </script>
  •  
  •  
  • <!-- Template for ngInclude. -->
  • <script type="text/ng-template" id="two.htm">
  •  
  • <div>
  • Template Two
  • </div>
  •  
  • </script>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.4.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 ) {
  •  
  • // I determine which subview to render.
  • $scope.subview = "one";
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I show the given subview.
  • $scope.showSubview = function( newSubview ) {
  •  
  • $scope.subview = newSubview;
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

This works, but add weight and complexity to the HTML page.

From what I've read, I get the sense that this breaking change was made specifically for the Animate feature. I guess it makes the animation easier to calculate when only one directive can possibly require animation at a time? I'm not sure. I haven't really dug into the animation yet. I just hope that at the end of the day, animation in AngularJS is not a Pyrrhic victory.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@All, part of why I find this breaking-change so devastating is that I use the ngSwitchWhen/ngInclude all over the place. You can see this in my nested-route demo on GitHub:

https://github.com/bennadel/AngularJS-Routing

If / when I upgrade to AngularJS 1.2, it basically means that every single rendered page in my applications will have to be re-worked.

Reply to this Comment

Lucky for you there's an easy work-around. The Angular team knows about the problem and they've included workarounds to fix this issue with ng-repeat+ng-include and ngif+nginclude but they have not added a work-around for ngswitch+ng-include.

To get past the assert, they add a special parameter to the directive:

  • $$tlb:true

.

So all you need to do is copy ngSwitchWhen directive, add that attribute, and rename it to something like bnSwitchWhen. Then a mass-find-replace for ng-switch-when and bn-switch-when should fix your problem. Once they've fixed this, you can simply undo your find-and-replace.

See the relevant work-around commit here: https://github.com/btford/angular.js/commit/becd7f9caea94597f8e09da037eaeed799c718d2

And here is where the issue is being tracked: https://github.com/angular/angular.js/issues/4357

Reply to this Comment

by the way, I bet if you rolled all that up into a PR, they'd accept it since they've done so for ng-repeat and ngif.

Reply to this Comment

@Jonathan,

I had seem some references to the "$$tlb" work-around when looking at the discussions regarding ngRepeat/ngInclude. But, I never really found any explanation of how it worked, or what precautions to take when using it. I'll take a look at your links.

I don't really have a problem with breaking code going from one version to another. I think the part of this that gets under my skin is the reason (from what I can gather) all of these breaking changes were done: Animation.

Stop reading if you dislike rants....

Maybe I'm just an old curmudgeon at this point, but animation just doesn't strike me as a good reason to really change the way code works... especially when we already could create directives that handled animation.

Of course, animation of certain types was difficult due to the black-box nature of some existing directives (like ngRepeat). However, I'm not sure that the new way makes much more sense. Though, I must caveat this with the fact that I have NOT started looking into animation yet.

With ngRepeat, it will _never_ make sense to animate every DOM-addition. That would be a silly UI. Really, it will only ever make sense to animate _new_ items being added to an existing, already-rendered list. The intent of animation is to help build a user's mental model. Animating new items builds a mental model... animating ALL items does nothing but slow down rendering.

That said, if you can easily and conditionally turn on/off animation for some elements in an ngRepeat, then I withdraw everything I just said :D ....

Ok, sorry for going off on the rant there.

Reply to this Comment

I feel your pain. I just got hit in the mouth myself after upgrading to 1.2.10. I was using nested ngSwitch and ngInclude in quite a few places across 4 apps. The size of several of my partials have literally quadrupled.

Reply to this Comment

Yeah this hit our team too and locked us as 1.1.5. It seemed like an irresponsible unilateral decision, but our graver concern is the future decisions the Angular team will make with other fundamental assumptions like this.

The 'use the stable branch' rebuttal really doesn't live in reality of any open source ecosystem IMO.

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.