Almost infinite recursive template in AngularJS for representing tree structures
Representing a tree-like structure can be a tricky thing, but as plenty of other examples found on the Internet show, people need to do it. Recently I also needed to do that and it worked quite well till the people who built the data have added another level to the tree. Then it blew up with a horrible error message. It was so big that the link leading to the Angular Error report site was also broken.
After some additional research I found the solution. Let's see it.
Recursive template with static data
First let's see the "standard" solution showing tree stuctures:
Our JavaScript code looks like this:
The HTML, including the template look like this:
examples/angular/recursive_template/recursive.html
the tree_view is the name of the template and it includes itself.
You can click on the try link to see the result.
In order to demonstrate the problem, and to avoid creating a large data structure manually,
I've created another example in which we generate the same tree structure using a
recursive JavaScript function. There are some logging messages included to make
it easier to see what's going on. (Read: I spent quite a few minutes getting the function right
and needed those printouts.)
examples/angular/recursive_template/recursive_generated.js
Besides the name of the included JavaScript file, the HTML did not change:
examples/angular/recursive_template/recursive_generated.html
As long as we give a number less than 10 to the function, which means the number of
levels in the tree, everything works find. Once we pass 10 or any larger number we
get a nasty error in the JavaScript console.
If you are lucky and the data structure isn't
too big, like in this example, the error will include a link that, after further
cleaning leads to
It reads:
In a nutshell: we had a recursive call in Angular, (this one we knew, this is what we wanted),
but in order to protect us from infinitely loops AngularJS (as many programming languages too)
imposes an arbitrary limit to the number of recursive calls. In the case of AngularJS this number is 10.
So one we reach this depth, AngularJS stops the recursion and reports the error.
At the end of the page it also suggests that we can adjust the limit through the
$rootScopeProvider.
The solution is then to increase the limit to something that matches our need.
Increasing this number means that in case you get in a unwanted recursive call
Angular will work harder before putting an end to the ordeal which might mean
that the application will be less responsive, so change it with care.
In this example I've increased the value to 14 which will be enough for our example:
Here is the full JavaScript code:
examples/angular/recursive_template/recursive_generated_fixed.js
Here you can try it:
examples/angular/recursive_template/recursive_generated_fixed.html
<!DOCTYPE html>
<html lang="en" ng-app="DemoApp">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, user-scalable=yes">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="recursive.js"></script>
<script type="text/ng-template" id="tree_view">
{{ node.title }}
<ul ng-if="node.tree">
<li ng-repeat="node in node.tree" ng-include="'tree_view'">
</li>
</ul>
</script>
</head>
<body ng-controller="DemoController">
<ul>
<li ng-repeat="node in tree" ng-include="'tree_view'"></li>
</ul>
</body>
</html>
Try!Recursive template with generated data
angular.module('DemoApp', [])
.controller('DemoController', function($scope, $log) {
var build_tree = function(depth) {
$log.log('build_tree', depth);
var leaf = [
{
title: "Level " + depth
}
]
if (depth > 1) {
leaf[0].tree = build_tree(depth-1);
}
return leaf;
}
$scope.tree = build_tree(10);
$log.log($scope.tree);
});
<!DOCTYPE html>
<html lang="en" ng-app="DemoApp">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, user-scalable=yes">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="recursive_generated.js"></script>
<script type="text/ng-template" id="tree_view">
{{ node.title }}
<ul ng-if="node.tree">
<li ng-repeat="node in node.tree" ng-include="'tree_view'">
</li>
</ul>
</script>
</head>
<body ng-controller="DemoController">
<ul>
<li ng-repeat="node in tree" ng-include="'tree_view'"></li>
</ul>
</body>
</html>
Try!
Error: $rootScope:infdig Infinite $digest Loop
10 $digest() iterations reached. Aborting!
Increase the recursion limit by setting $rootScopeProvider.digestTtl
.config(function($rootScopeProvider){
$rootScopeProvider.digestTtl(14);
})
angular.module('DemoApp', [])
.config(function($rootScopeProvider){
$rootScopeProvider.digestTtl(14);
})
.controller('DemoController', function($scope, $log) {
var build_tree = function(depth) {
$log.log('build_tree', depth);
var leaf = [
{
title: "Level " + depth
}
]
if (depth > 1) {
leaf[0].tree = build_tree(depth-1);
}
return leaf;
}
$scope.tree = build_tree(10);
$log.log($scope.tree);
});
<!DOCTYPE html>
<html lang="en" ng-app="DemoApp">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, user-scalable=yes">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="recursive_generated_fixed.js"></script>
<script type="text/ng-template" id="tree_view">
{{ node.title }}
<ul ng-if="node.tree">
<li ng-repeat="node in node.tree" ng-include="'tree_view'">
</li>
</ul>
</script>
</head>
<body ng-controller="DemoController">
<ul>
<li ng-repeat="node in tree" ng-include="'tree_view'"></li>
</ul>
</body>
</html>
Try!Screenshots
Published on 2016-05-18