Skip to main content
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: John Mason
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: John Mason ( @john_mason_ )

ngInclude Asynchronous Template Life Cycle Bug In AngularJS

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

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel