Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Ed Bartram and Anne Porosoff
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Ed Bartram@edbartram ) and Anne Porosoff@AnnePorosoff )

ngInclude Asynchronous Template Life Cycle Bug In AngularJS

By Ben Nadel on

When possible, I try to inline my AngularJS templates so that they are immediately available for consumption in the $compile() service. However, at InVision, we recently switched to a local development workflow that defers template access. This means that templates are loaded, via AJAX (implicitly by AngularJS), only when they are needed. After we made this switch, I started seeing new problems show up. After some debugging, I narrowed it down to what I consider to be a bug in the ngInclude directive template life cycle in AngularJS.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The bug in question is a race condition. If an AngularJS scope is destroyed before an associated ngInclude remote template has returned, directives embedded within that remote content are linked but never destroyed. This means that event-handlers, that depend on the $destroy event, are never cleaned-up.

Because this is a race condition, it's possible that you've never seen this occur. And, if you inline your templates, then I don't believe that there is a chance that this can actually occur for you. But, it's easy enough to demonstrate. In the following code, we're going to trigger a template request and then immediately destroy the parent scope:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • ngInclude Asynchronous Template Life Cycle Bug In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • ngInclude Asynchronous Template Life Cycle Bug In AngularJS
  • </h1>
  •  
  • <div ng-switch="template">
  • <div ng-switch-when="remote">
  •  
  • <!-- This remote template includes the embedded directive, bn-test. -->
  • <div ng-include=" './remote.htm' "></div>
  •  
  • </div>
  • <div ng-switch-when="local">
  •  
  • <p>
  • Showing inline content.
  • </p>
  •  
  • </div>
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.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.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function( $scope, $timeout ) {
  •  
  • // Start out showing the remote template (which contains a directive).
  • $scope.template = "remote";
  •  
  • // *Almost immediately* switch to showing the local template.
  • $timeout(
  • function changeTemplate() {
  •  
  • $scope.template = "local";
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I log the scope-life-cycle events of the context element.
  • angular.module( "Demo" ).directive(
  • "bnTest",
  • function() {
  •  
  • // Return the directive configuration object.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attribute ) {
  •  
  • console.log( "Directive linked." );
  •  
  • scope.$on(
  • "$destroy",
  • function handleDestroy() {
  •  
  • console.log( "Directive destroyed." );
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

In this case, we are using the ngSwitch directive to trigger the remote template load; then, we [almost] immediately switch over to a different template. When the ngInclude remote template returns, however, the bnTest directive is linked but never destroyed, leaving us with the console output:

Directive linked.

Notice that the console log item, "Directive destroyed", never shows up. As such, any embedded directives, within the remote content, are never torn-down.

This is a pretty small edge-case; but, it's also something that is probably easy to fix (presumably by checking to see if the scope is "$$destroyed" before actually transcluding the content). And, just to be clear, I don't believe that this occurs for "component directives" (ie, directives that use templateUrl); this seems to be limited to ngInclude.




Reader Comments

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.