By Benny Bottema


2013-01-20 23:40:51 8 Comments

There are a couple of popular recursive angular directive Q&A's out there, which all come down to one of the following solutions:

The first one has the problem that you can't remove previously compiled code unless you comprehensibly manage the manual compile process. The second approach has the problem of... not being a directive and missing out on its powerful capabilities, but more urgently, it can't be parameterised the same way a directive can be; it's simply bound to a new controller instance.

I've been playing with manually doing an angular.bootstrap or @compile() in the link function, but that leaves me with the problem of manually keeping track of elements to remove and add.

Is there a good way to have a parameterized recursive pattern that manages adding/removing elements to reflect runtime state? That is to say, a tree with a add/delete node button and some input field whose value is passed down a node's child nodes. Perhaps a combination of the second approach with chained scopes (but I have no idea how to do this)?

9 comments

@Mark Lagendijk 2013-09-04 09:04:06

Inspired by the solutions described in the thread mentioned by @dnc253, I abstracted the recursion functionality into a service.

module.factory('RecursionHelper', ['$compile', function($compile){
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param element
         * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function(element, link){
            // Normalize the link parameter
            if(angular.isFunction(link)){
                link = { post: link };
            }

            // Break the recursion loop by removing the contents
            var contents = element.contents().remove();
            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function(scope, element){
                    // Compile the contents
                    if(!compiledContents){
                        compiledContents = $compile(contents);
                    }
                    // Re-add the compiled contents to the element
                    compiledContents(scope, function(clone){
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if(link && link.post){
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);

Which is used as follows:

module.directive("tree", ["RecursionHelper", function(RecursionHelper) {
    return {
        restrict: "E",
        scope: {family: '='},
        template: 
            '<p>{{ family.name }}</p>'+
            '<ul>' + 
                '<li ng-repeat="child in family.children">' + 
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(element) {
            // Use the compile function from the RecursionHelper,
            // And return the linking function(s) which it returns
            return RecursionHelper.compile(element);
        }
    };
}]);

See this Plunker for a demo. I like this solution best because:

  1. You don't need an special directive which makes your html less clean.
  2. The recursion logic is abstracted away into the RecursionHelper service, so you keep your directives clean.

Update: As of Angular 1.5.x, no more tricks are required, but works only with template, not with templateUrl

@jssebastian 2013-09-26 17:25:48

Thanks, great solution! really clean and worked out of the box for me to make recursion between two directives that include each other work.

@prototype 2013-11-08 18:51:05

Minor note, attributes with hyphens in nested directives (i.e. that would normally get changed to camelCase) were not getting set (e.g. <paint base-color="white"></paint>), as the link function seems not to be getting called. Solution was to remove hyphens (e.g. (e.g. <paint basecolor="white"></paint>)

@Mark Lagendijk 2013-11-29 15:36:42

The original problem is that when you use recursive directives AngularJS gets into an endless loop. This code breaks this loop by removing the contents during the compile event of the directive, and compiling and re-adding the contents in the link event of the directive.

@Benny Bottema 2013-12-11 19:48:04

Although SunnyShah helpt me greatly to understand the problem, this solution is awesome. "and re-adding the contents in the link event of the directive." Although not entirely obvious in the code, the compile function in the RecursionHelper returns a function which AngularJs will use as link function.

@Paolo Moretti 2014-02-11 16:28:13

In your example you could replace compile: function(element) { return RecursionHelper.compile(element); } with compile: RecursionHelper.compile.

@tardyp 2014-03-14 14:14:59

I agree with Paolo that his syntax is indeed more simple and does not add confusion. Great solution, which should be accepted as the top solution rather than the recursive directive. @Plantface: would you agree to put it top solution?

@peinearydevelopment 2014-07-15 22:43:16

Thanks so much for your suggestion. I tried updating it with my use case and am running into difficulties. Can anyone tell me what I'm doing wrong? plnkr.co/edit/AvB2mHxE4OGDu2YBJh9M?p=preview I would expect to have 2. filled in with more input boxes.

@webaba 2014-11-04 12:37:33

This is awesome but doesn't work when using transclude, I posted a pull-request to fix that.

@Riad Baghbanli 2014-11-26 04:02:30

Great work, nicely separated recursion service. One have to be careful if link function contains $scope.$watch on model data returned from $http call. If there is check for new val != old val, then that is only going to update root element. Checking for valid new val should be used instead.

@CodyBugstein 2015-03-04 13:12:06

What if you want the template to be located in an external file?

@Carlo Bonamico 2015-05-15 21:48:16

This is elegant in the sense that if/when Angular core implements a similar support, you can just remove the custom compile wrapper and all remaining code would remain the same.

@user1455719 2015-06-16 10:18:58

I am really greatful to the author of this post. It saved my job. I had an app built with angularjs and it uses extensive recursive functions. Some times my page was taking more than a minute to load. I modified my code as per the suggestions in this post and my page is getting loaded in 5 seconds. thanks Mark Lagendijk ...I don't know who you are or where do u live, but am sure that you will be an asset to any organization

@plong0 2015-12-18 20:12:55

@PaoloMoretti the shorthand you suggested also does not allow defining a link function...

@Benny Bottema 2016-01-30 12:55:49

@tutley recursion is limited to the data being fed, which is finite by definition. Limiting your data further by filtering or culling means decreasing the recursion maximum.

@Joshua Taylor 2016-01-30 20:24:59

@Plantface I agree that the data should be finite, but there's nothing that says a Javascript object can't be self-referential. All it takes is var x = {}; x.y = x; and you've got data that you can recurse infinitely through.

@anion 2016-02-19 07:13:02

is it possible that all tree-directive-instances have one single controller-instance in this solution? if yes: how can i fix it, so that every tree-node has its own controller-instance?

@Mzn 2016-05-17 07:09:39

I'm relatively an Angular noob. I don't understand why all this is required. Recursion should work out of the box since the SCOPE of the inner directive (same directive (recursive) or another) is NEWED when specifying scope: { something: '=' }... Can someone explain this in basic terms?

@clopez 2017-03-13 14:27:50

I just created a Gist referencing to this code: gist.github.com/decklord/26037e31f7b0ad7bb5c7806296ed63b3

@Slim 2017-06-14 15:23:13

It worked but seems it requires single root element in the recursive directive's template without ng-repeat on it.

@Jayani Sumudini 2018-04-20 05:11:15

It's worked.Thanks..very clear and working solution.thanks again

@erobwen 2015-08-24 20:44:03

There is a really really simple workaround for this that does not require directives at all.

Well, in that sense, maybe it is not even a solution of the original problem if you assume you need directives, but it IS a solution if you want a recursive GUI structure with parametrized sub-structures of the GUI. Which is probably what you want.

The solution is based on just using ng-controller, ng-init and ng-include. Just do it as follows, assume that your controller is called "MyController", your template is located in myTemplate.html and that you have an initialization function on your controller called init that takes argument A, B, and C, making it possible to parametrize your controller. Then the solution is as follows:

myTemplate.htlm:

<div> 
    <div>Hello</div>
    <div ng-if="some-condition" ng-controller="Controller" ng-init="init(A, B, C)">
       <div ng-include="'myTemplate.html'"></div>
    </div>
</div>

I found by plain conincidence that this kind of structure can be made recursive as you like in plain vanilla angular. Just follow this design pattern and you can use recursive UI-structures without any advanced compilation tinkering etc.

Inside your controller:

$scope.init = function(A, B, C) {
   // Do something with A, B, C
   $scope.D = A + B; // D can be passed on to other controllers in myTemplate.html
} 

The only downside I can see is the clunky syntax you have to put up with.

@Stewart_R 2016-05-05 16:03:21

I'm afraid this fails to solve the problem in a rather fundamental way: With this approach you would need to know the depth of the recursion up front in order to have enough controllers in myTemplate.html

@erobwen 2017-01-17 11:48:27

Actually, you don't. Since your file myTemplate.html contains a self reference to myTemplate.html using ng-include (the html contents above is the contents of myTemplate.html, perhaps not clearly stated). That way it becomes truly recursive. I have used the technique in production.

@erobwen 2017-01-17 11:52:32

Also, perhaps not clearly stated is that you also need to use ng-if somewhere to terminate the recursion. So your myTemplate.html is then of the form as updated in my comment.

@jkris 2016-07-28 09:34:41

As of Angular 1.5.x, no more tricks are required, the following has been made possible. No more need for dirty work arounds!

This discovery was a by product of my hunt for a better/cleaner solution for a recursive directive. You can find it here https://jsfiddle.net/cattails27/5j5au76c/. It supports as far is 1.3.x.

angular.element(document).ready(function() {
  angular.module('mainApp', [])
    .controller('mainCtrl', mainCtrl)
    .directive('recurv', recurveDirective);

  angular.bootstrap(document, ['mainApp']);

  function recurveDirective() {
    return {
      template: '<ul><li ng-repeat="t in tree">{{t.sub}}<recurv tree="t.children"></recurv></li></ul>',
      scope: {
        tree: '='
      },
    }
  }

});

  function mainCtrl() {
    this.tree = [{
      title: '1',
      sub: 'coffee',
      children: [{
        title: '2.1',
        sub: 'mocha'
      }, {
        title: '2.2',
        sub: 'latte',
        children: [{
          title: '2.2.1',
          sub: 'iced latte'
        }]
      }, {
        title: '2.3',
        sub: 'expresso'
      }, ]
    }, {
      title: '2',
      sub: 'milk'
    }, {
      title: '3',
      sub: 'tea',
      children: [{
        title: '3.1',
        sub: 'green tea',
        children: [{
          title: '3.1.1',
          sub: 'green coffee',
          children: [{
            title: '3.1.1.1',
            sub: 'green milk',
            children: [{
              title: '3.1.1.1.1',
              sub: 'black tea'
            }]
          }]
        }]
      }]
    }];
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<div>
  <div ng-controller="mainCtrl as vm">
    <recurv tree="vm.tree"></recurv>
  </div>
</div>

@Steven 2016-08-28 17:31:40

Thanks for this. Could you link me to the changelog that introduced this feature? Thanks!

@Paqman 2016-09-01 12:30:40

Using angular 1.5.x is very important. 1.4.x won't work and is actually the version provided in jsfiddle.

@Paolo Biavati 2017-05-05 11:14:23

in the the jsfiddle jsfiddle.net/cattails27/5j5au76c there is not the same code of this answer... is it right? what I'm missing?

@jkris 2017-05-08 03:13:57

The fiddle shows for angular versions less than 1.5x

@Jens 2014-06-09 18:16:15

I ended up creating a set of basic directives for recursion.

IMO It is far more basic than the solution found here, and just as flexible if not more, so we are not bound to using UL/LI structures etc... But obviously those make sense to use, however the directives are unaware of this fact...

A Super simple example would be:

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

The implementation of 'dx-start-with' an 'dx-connect' is found at: https://github.com/dotJEM/angular-tree

This means you don't have to create 8 directives if you need 8 different layouts.

To create a tree-view on top of that where you can add or delete nodes would then be rather simple. As in: http://codepen.io/anon/pen/BjXGbY?editors=1010

angular
  .module('demo', ['dotjem.angular.tree'])
  .controller('AppController', function($window) {

this.rootNode = {
  name: 'root node',
  children: [{
    name: 'child'
  }]
};

this.addNode = function(parent) {
  var name = $window.prompt("Node name: ", "node name here");
  parent.children = parent.children || [];
  parent.children.push({
    name: name
  });
}

this.removeNode = function(parent, child) {
  var index = parent.children.indexOf(child);
  if (index > -1) {
    parent.children.splice(index, 1);
  }
}

  });
<div ng-app="demo" ng-controller="AppController as app">
  HELLO TREE
  <ul dx-start-with="app.rootNode">
<li><button ng-click="app.addNode($dxPrior)">Add</button></li>
<li ng-repeat="node in $dxPrior.children">
  {{ node.name }} 
  <button ng-click="app.removeNode($dxPrior, node)">Remove</button>
  <ul dx-connect="node" />
</li>
  </ul>

  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
  <script src="https://rawgit.com/dotJEM/angular-tree-bower/master/dotjem-angular-tree.min.js"></script>

</div>

From this point on, the controller and template could be wrapped in it's own directive if one would wish for it.

@Oleksandr Knyga 2015-06-12 13:35:22

You can use angular-recursion-injector for that: https://github.com/knyga/angular-recursion-injector

Allows you to do unlimited depth nesting with conditioning. Does recompilation only if needed and compiles only right elements. No magic in code.

<div class="node">
  <span>{{name}}</span>

  <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

One of the things that allows it to work faster and simpler then the other solutions is "--recursion" suffix.

@TGH 2015-05-30 22:01:13

Now that Angular 2.0 is out in preview I think it's ok to add an Angular 2.0 alternative into the mix. At least it will benefit people later:

The key concept is to build a recursive template with a self reference:

<ul>
    <li *for="#dir of directories">

        <span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()"    /></span> 
        <span (click)="dir.toggle()">{{ dir.name }}</span>

        <div *if="dir.expanded">
            <ul *for="#file of dir.files">
                {{file}}
            </ul>
            <tree-view [directories]="dir.directories"></tree-view>
        </div>
    </li>
</ul>

You then bind a tree object to the template and watch the recursion take care of the rest. Here is a full example: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

@tilgovi 2015-04-19 21:35:12

After using several workarounds for a while, I've repeatedly come back to this issue.

I'm not satisfied by the service solution since it works for directives that can inject the service but does not work for anonymous template fragments.

Similarly, solutions which depend upon specific template structure by doing DOM manipulation in the directive are too specific and brittle.

I have what I believe is a generic solution that encapsulates the recursion as a directive of its own that interferes minimally with any other directives and can be used anonymously.

Below is a demonstration that you can also play around with at plnkr: http://plnkr.co/edit/MSiwnDFD81HAOXWvQWIM

var hCollapseDirective = function () {
  return {
    link: function (scope, elem, attrs, ctrl) {
      scope.collapsed = false;
      scope.$watch('collapse', function (collapsed) {
        elem.toggleClass('collapse', !!collapsed);
      });
    },
    scope: {},
    templateUrl: 'collapse.html',
    transclude: true
  }
}

var hRecursiveDirective = function ($compile) {
  return {
    link: function (scope, elem, attrs, ctrl) {
      ctrl.transclude(scope, function (content) {
        elem.after(content);
      });
    },
    controller: function ($element, $transclude) {
      var parent = $element.parent().controller('hRecursive');
      this.transclude = angular.isObject(parent)
        ? parent.transclude
        : $transclude;
    },
    priority: 500,  // ngInclude < hRecursive < ngIf < ngRepeat < ngSwitch
    require: 'hRecursive',
    terminal: true,
    transclude: 'element',
    $$tlb: true  // Hack: allow multiple transclusion (ngRepeat and ngIf)
  }
}

angular.module('h', [])
.directive('hCollapse', hCollapseDirective)
.directive('hRecursive', hRecursiveDirective)
/* Demo CSS */
* { box-sizing: border-box }

html { line-height: 1.4em }

.task h4, .task h5 { margin: 0 }

.task { background-color: white }

.task.collapse {
  max-height: 1.4em;
  overflow: hidden;
}

.task.collapse h4::after {
  content: '...';
}

.task-list {
  padding: 0;
  list-style: none;
}


/* Collapse directive */
.h-collapse-expander {
  background: inherit;
  position: absolute;
  left: .5px;
  padding: 0 .2em;
}

.h-collapse-expander::before {
  content: '•';
}

.h-collapse-item {
  border-left: 1px dotted black;
  padding-left: .5em;
}

.h-collapse-wrapper {
  background: inherit;
  padding-left: .5em;
  position: relative;
}
<!DOCTYPE html>
<html>

  <head>
    <link href="collapse.css" rel="stylesheet" />
    <link href="style.css" rel="stylesheet" />
    <script data-require="[email protected]" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js" data-semver="2.1.1" data-require="[email protected]*"></script>
    <script src="script.js"></script>
    <script>
      function AppController($scope) {
        $scope.toggleCollapsed = function ($event) {
          $event.preventDefault();
          $event.stopPropagation();
          this.collapsed = !this.collapsed;
        }
        
        $scope.task = {
          name: 'All tasks',
          assignees: ['Citizens'],
          children: [
            {
              name: 'Gardening',
              assignees: ['Gardeners', 'Horticulture Students'],
              children: [
                {
                  name: 'Pull weeds',
                  assignees: ['Weeding Sub-committee']
                }
              ],
            },
            {
              name: 'Cleaning',
              assignees: ['Cleaners', 'Guests']
            }
          ]
        }
      }
      
      angular.module('app', ['h'])
      .controller('AppController', AppController)
    </script>
  </head>

  <body ng-app="app" ng-controller="AppController">
    <h1>Task Application</h1>
    
    <p>This is an AngularJS application that demonstrates a generalized
    recursive templating directive. Use it to quickly produce recursive
    structures in templates.</p>
    
    <p>The recursive directive was developed in order to avoid the need for
    recursive structures to be given their own templates and be explicitly
    self-referential, as would be required with ngInclude. Owing to its high
    priority, it should also be possible to use it for recursive directives
    (directives that have templates which include the directive) that would
    otherwise send the compiler into infinite recursion.</p>
    
    <p>The directive can be used alongside ng-if
    and ng-repeat to create recursive structures without the need for
    additional container elements.</p>
    
    <p>Since the directive does not request a scope (either isolated or not)
    it should not impair reasoning about scope visibility, which continues to
    behave as the template suggests.</p>
    
    <p>Try playing around with the demonstration, below, where the input at
    the top provides a way to modify a scope attribute. Observe how the value
    is visible at all levels.</p>
    
    <p>The collapse directive is included to further demonstrate that the
    recursion can co-exist with other transclusions (not just ngIf, et al)
    and that sibling directives are included on the recursive due to the
    recursion using whole 'element' transclusion.</p>
    
    <label for="volunteer">Citizen name:</label>
    <input id="volunteer" ng-model="you" placeholder="your name">
    <h2>Tasks</h2>
    <ul class="task-list">
      <li class="task" h-collapse h-recursive>
        <h4>{{task.name}}</h4>
        <h5>Volunteers</h5>
        <ul>
          <li ng-repeat="who in task.assignees">{{who}}</li>
          <li>{{you}} (you)</li>
        </ul>
        <ul class="task-list">
          <li h-recursive ng-repeat="task in task.children"></li>
        </ul>
      <li>
    </ul>
    
    <script type="text/ng-template" id="collapse.html">
      <div class="h-collapse-wrapper">
        <a class="h-collapse-expander" href="#" ng-click="collapse = !collapse"></a>
        <div class="h-collapse-item" ng-transclude></div>
      </div>
    </script>
  </body>

</html>

@SunnyShah 2013-01-22 14:01:21

Manually adding elements and compiling them is definitely a perfect approach. If you use ng-repeat then you will not have to manually remove elements.

Demo: http://jsfiddle.net/KNM4q/113/

.directive('tree', function ($compile) {
return {
    restrict: 'E',
    terminal: true,
    scope: { val: '=', parentData:'=' },
    link: function (scope, element, attrs) {
        var template = '<span>{{val.text}}</span>';
        template += '<button ng-click="deleteMe()" ng-show="val.text">delete</button>';

        if (angular.isArray(scope.val.items)) {
            template += '<ul class="indent"><li ng-repeat="item in val.items"><tree val="item" parent-data="val.items"></tree></li></ul>';
        }
        scope.deleteMe = function(index) {
            if(scope.parentData) {
                var itemIndex = scope.parentData.indexOf(scope.val);
                scope.parentData.splice(itemIndex,1);
            }
            scope.val = {};
        };
        var newElement = angular.element(template);
        $compile(newElement)(scope);
        element.replaceWith(newElement);
    }
}
});

@Benny Bottema 2013-01-22 16:41:35

I updated your script so that it has only one directive. jsfiddle.net/KNM4q/103 How can we make that delete button work?

@SunnyShah 2013-01-22 18:39:00

@Benny Bottema 2013-01-22 19:36:13

Very nice! I was very close, but didn't have @position (I thought I could find it with parentData[val]. If you update your answer with the final version (jsfiddle.net/KNM4q/111) I'll accept it.

@Benny Bottema 2013-01-22 19:41:37

Turns out it works without position also: jsfiddle.net/KNM4q/113

@dnc253 2013-02-02 00:30:54

I don't know for sure if this solution is found in one of the examples you linked or the same basic concept, but I had a need of a recursive directive, and I found a great, easy solution.

module.directive("recursive", function($compile) {
    return {
        restrict: "EACM",
        priority: 100000,
        compile: function(tElement, tAttr) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents);
                }
                iElement.append(
                    compiledContents(scope, 
                                     function(clone) {
                                         return clone; }));
            };
        }
    };
});

module.directive("tree", function() {
    return {
        scope: {tree: '='},
        template: '<p>{{ tree.text }}</p><ul><li ng-repeat="child in tree.children"><recursive><span tree="child"></span></recursive></li></ul>',
        compile: function() {
            return  function() {
            }
        }
    };
});​

You should create the recursive directive and then wrap it around the element that makes the recursive call.

@Jack 2013-11-29 01:23:33

@MarkError and @dnc253 this is helpful, however I always receive the following error: [$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">

@Jack 2013-11-29 18:03:51

If anyone else is experiencing this error, just you (or Yoeman) hasn't included any JavaScript files more than once. Somehow my main.js file was included twice and therefore two directives with the same name were being created. After removing one of the JS includes, the code worked.

@Beyers 2013-12-05 01:25:55

@Jack Thanks for pointing that out. Just spend a number of hours trouble shooting this issue and your comment pointed me into the right direction. For ASP.NET users making use of bundling service, make sure you dont have an old minified version of a file in the directory while you use wildcard includes in bundling.

@Tsuneo Yoshioka 2015-11-30 09:15:08

For me, element is needed to add inside callback like: compiledContents(scope,function(clone) { iElement.append(clone); }); .Otherwise, "require"ed controller is not correctly handled, and error: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found! cause.

@Learning-Overthinker-Confused 2017-05-02 14:34:32

I am trying to generate tree structure with angular js but stuck with that.

Related Questions

Sponsored Content

27 Answered Questions

[SOLVED] What is tail recursion?

18 Answered Questions

3 Answered Questions

[SOLVED] How can I tell AngularJS to "refresh"

15 Answered Questions

2 Answered Questions

3 Answered Questions

[SOLVED] Placement of the ng-app directive (html vs body)

  • 2013-04-03 14:39:59
  • MattDavis
  • 37360 View
  • 103 Score
  • 3 Answer
  • Tags:   angularjs

0 Answered Questions

Angular compile new dom with directive

1 Answered Questions

[SOLVED] AngularJS - Does $destroy remove event listeners?

1 Answered Questions

Angular Recursion

0 Answered Questions

Angular JS :Recursive directive pre-processing

Sponsored Content