Search for '{{search_term}}'

Almost infinite recursive template in AngularJS for representing tree structures

CMOS is the Code-Maven Open Source podcast that also includes video interviews. Subscribe to this feed RSS feed with your Podcast listener app or via iTunes iTunes.

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:

It doesn't have much in it, just the data structure.

The HTML, including the template look like this:

examples/angular/recursive_template/recursive.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!

the tree_view is the name of the template and it includes itself.

You can click on the try link to see the result.

Recursive template with generated data

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

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);
});

Besides the name of the included JavaScript file, the HTML did not change:

examples/angular/recursive_template/recursive_generated.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_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!

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 this error page

It reads:

Error: $rootScope:infdig Infinite $digest Loop
10 $digest() iterations reached. Aborting!

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.

Increase the recursion limit by setting $rootScopeProvider.digestTtl

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:

.config(function($rootScopeProvider){
    $rootScopeProvider.digestTtl(14);
})

Here is the full JavaScript code:

examples/angular/recursive_template/recursive_generated_fixed.js

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);
});

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_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

Comments

In the comments, please wrap your code snippets within <pre> </pre> tags and use spaces for indentation.
comments powered by Disqus