首页 文章

Angular指令中的递归

提问于
浏览
174

有一些流行的递归角度指令Q&A,它们都归结为以下解决方案之一:

The first one 有一个问题,除非您可理解地管理手动编译过程,否则无法删除以前编译的代码 . The second approach 的问题是......不是一个指令,错过了它强大的功能,但更紧迫的是,它可以简单地绑定到一个新的控制器实例 .

我一直在玩链接功能中手动执行 angular.bootstrap@compile() ,但这让我遇到了手动跟踪要删除和添加的元素的问题 .

有一种很好的方法可以使用参数化递归模式来管理添加/删除元素以反映运行时状态吗?也就是说,一个带有添加/删除节点按钮的树和一些输入字段,其值从节点的子节点传递下来 . 也许是第二种方法与链式范围的组合(但我不知道如何做到这一点)?

9 回答

  • 12

    受@ dnc253提到的线程中描述的解决方案的启发,我抽象了递归功能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);
                        }
                    }
                };
            }
        };
    }]);
    

    使用如下:

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

    有关演示,请参阅此Plunker . 我最喜欢这个解决方案,因为:

    • 您不需要特殊的指令来使您的html不那么干净 .

    • 递归逻辑被抽象到RecursionHelper服务中,因此您可以保持指令清洁 .

    更新:从Angular 1.5.x开始,不再需要技巧,但仅适用于模板,而不适用于templateUrl

  • 0

    手动添加元素并编译它们绝对是一种完美的方法 . 如果使用ng-repeat,则无需手动删除元素 .

    演示: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);
        }
    }
    });
    
  • 23

    我不确定这个解决方案是否在您链接的示例或相同的基本概念中找到,但我需要一个递归指令,我找到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() {
                }
            }
        };
    });​
    

    您应该创建 recursive 指令,然后将其包装在进行递归调用的元素周围 .

  • 0

    从Angular 1.5.x开始,不再需要花样,以下内容已经成为可能 . 不再需要肮脏的工作了!

    这个发现是我寻找更好/更清晰的递归指令解决方案的副产品 . 你可以在这里找到https://jsfiddle.net/cattails27/5j5au76c/ . 它支持到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>
    
  • 2

    在使用了几个变通方法之后,我一再回到这个问题 .

    我对服务解决方案不满意,因为它适用于可以注入服务但不适用于匿名模板片段的指令 .

    类似地,通过在指令中执行DOM操作而依赖于特定模板结构的解决方案太具体和脆弱 .

    我有一个我认为是通用的解决方案,它将递归封装为自己的指令,它最低限度地干扰任何其他指令,并且可以匿名使用 .

    下面是一个演示,你也可以在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="angular.js@1.3.15" 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="jquery@*"></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>
    
  • 10

    现在Angular 2.0已经预览了,我认为可以在混合中添加Angular 2.0替代品 . 至少它会让人们以后受益:

    关键概念是使用自引用构建递归模板:

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

    然后,将树对象绑定到模板,并观察递归处理其余部分 . 这是一个完整的例子:http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

  • 313

    有一个非常简单的解决方法,根本不需要指令 .

    那么,从这个意义上说,如果你假设你需要指令,它甚至可能不是原始问题的解决方案,但如果你想要一个带有参数化GUI子结构的递归GUI结构,它就是一个解决方案 . 这可能是你想要的 .

    该解决方案仅基于ng-controller,ng-init和ng-include . 只需执行以下操作,假设您的控制器名为“MyController”,您的模板位于myTemplate.html中,并且您的控制器上有一个名为init的初始化函数,它接受参数A,B和C,从而可以参数化您的控制器 . 然后解决方案是如下:

    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>
    

    我通过简单的巧合发现,这种结构可以按照你喜欢的普通香草角度进行递归 . 只需遵循这种设计模式,您就可以使用递归的UI结构而无需任何高级编译修改等 .

    在您的控制器内:

    $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
    }
    

    我能看到的唯一缺点是你必须忍受的笨重的语法 .

  • 2

    你可以使用angular-recursion-injector:https://github.com/knyga/angular-recursion-injector

    允许您通过调节进行无限深度嵌套 . 仅在需要时重新编译并仅编译正确的元素 . 代码中没有魔力 .

    <div class="node">
      <span>{{name}}</span>
    
      <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
    </div>
    

    其中一个允许它比其他解决方案更快更简单的工作是“--recursion”后缀 .

  • 4

    我最终创建了一组递归的基本指令 .

    IMO它比这里找到的解决方案要基本得多,而且如果不是更灵活,那么我们也不一定要使用UL / LI结构等......但显然这些使用是有意义的,但指令却没有意识到这一点事实...

    一个非常简单的例子是:

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

    'dx-start-with' an 'dx-connect'的实现位于:https://github.com/dotJEM/angular-tree

    这意味着如果需要8种不同的布局,则不必创建8个指令 .

    要在可以添加或删除节点的位置创建树视图,将会非常简单 . 如: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>
    

    从这一点开始,控制器和模板可以包含在它自己的指令中,如果有人愿意的话 .

相关问题