Recursion in Angular directives

2018-12-31 06:10发布

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条回答
不再属于我。
2楼-- · 2018-12-31 06:56

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.

查看更多
流年柔荑漫光年
3楼-- · 2018-12-31 06:56

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.

查看更多
不再属于我。
4楼-- · 2018-12-31 06:57

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>

查看更多
初与友歌
5楼-- · 2018-12-31 06:57

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

查看更多
心情的温度
6楼-- · 2018-12-31 06:57

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.

查看更多
梦醉为红颜
7楼-- · 2018-12-31 07:00

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

查看更多
登录 后发表回答