Recursive Angular Directive

If a directive is recursive, you must use template url to avoid infinite loop.

An angular directive that refers to itself inside a template loaded separately causes an infinite linking loop, completely frying the browser. The page stays blank, and one cannot even open the development console to see the problem.

Working example

This directive foo works fine, because it never refers to itself recursively

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
<script>
angular.module('RecursiveDirective', [])
.directive('foo', function () {
return {
restrict: 'E',
template: 'Hello from foo'
};
});
</script>
</head>
<body ng-app="RecursiveDirective">
<h2>Recursive directive example</h2>
<foo></foo>
</body>
</html>

Recursive example with inline template

Let us change foo's template to include a reference to itself

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
<script>
angular.module('RecursiveDirective', [])
.directive('foo', function () {
return {
restrict: 'E',
template: 'Hello from <foo></foo>'
};
});
</script>
</head>
<body ng-app="RecursiveDirective">
<h2>Recursive directive example</h2>
<foo></foo>
</body>
</html>

This simple example shows lots of "Hello from" on the same page until a JavaScript exception is thrown

RangeError: Maximum call stack size exceeded
    at String.replace (native)

At least the browser stops the run away execution.

Recursive example with async template load

A more realistic example with a custom directive would keep its template in a separate file and access it through templateUrl. I always compile my templates using grunt-html2js to prepopulate $templateCache to avoid separate Ajax calls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
<script>
angular.module("templates", []).run(["$templateCache", function($templateCache) {
$templateCache.put("foo-template", "Hello from <foo></foo>");
}]);
angular.module('RecursiveDirective', ['templates'])
.directive('foo', function () {
return {
restrict: 'E',
templateUrl: 'foo-template'
};
});
</script>
</head>
<body ng-app="RecursiveDirective">
<h2>Recursive directive example</h2>
<foo></foo>
</body>
</html>

This simple page never shows anything! The Angularjs alternates between loading template and updating the DOM. Because these are separate operations through the event loop, and not a single JavaScript execution, the browser neither exceeds the maximum stack nor maximum single script execution time.

You cannot even see the problem or place a break point, even if you open the Chrome DevTools beforehand; the link function only executes once! The best strategy was to open the "Profiles" tab in DevTools and record a session on the unresponsive blank page. Then I could get an idea what the angularjs engine was spending time on. Here is a screenshot.

recursive directive profile

Notice the top 2 time sinks: setting node content and grabbing template's content.

Conclusion

Be careful when using directive's names inside the templates. This will cause infinite recursion that the browser does not stop. It can happen with cyclical dependencies too: if foo includes bar, and bar includes foo this can cause infinite linking loop.