Angular指令中的递归

180

有几个流行的递归Angular指令问答,它们都可以归结为以下解决方案之一:

第一个的问题是,除非你全面管理手动编译过程,否则无法删除以前编译的代码。 第二种方法的问题是...它不是指令,错过了它强大的功能,但更紧迫的是,它不能像指令一样被参数化;它只是绑定到一个新的控制器实例。

我一直在尝试手动在链接函数中执行angular.bootstrap@compile(),但这让我面临手动跟踪要删除和添加的元素的问题。

有没有一种好的方式来创建参数化递归模式,以管理添加/删除元素来反映运行时状态?也就是说,一个树形结构带有添加/删除节点的按钮和一些输入字段,其值传递给一个节点的子节点。也许可以将第二种方法与链式作用域相结合(但我不知道如何做到这一点)?

9个回答

319

受@dnc253提到的线程中描述的解决方案启发,我将递归功能抽象成了一个服务

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查看演示。

我认为这是最好的解决方案,因为:

  1. 您不需要特殊的指令来使您的HTML更加简洁。
  2. 递归逻辑被封装到RecursionHelper服务中,因此您可以保持指令的简洁性。

更新: 截至Angular 1.5.x,不再需要任何技巧,但仅适用于模板而非模板URL


3
谢谢,非常好的解决方案!对我来说非常干净,直接奏效,使得两个互相包含的指令之间的递归能够正常运行。 - jssebastian
6
原始问题在于当您使用递归指令AngularJS会进入无限循环。该代码通过在指令的编译事件期间删除内容,并在指令的链接事件中进行编译和重新添加内容,从而打破了这个循环。 - Mark Lagendijk
15
在您的例子中,您可以将 compile: function(element) { return RecursionHelper.compile(element); } 替换为 compile: RecursionHelper.compile - Paolo Moretti
2
这种方法的优雅之处在于,如果/当Angular核心实现类似的支持时,您只需删除自定义编译包装器,所有剩余的代码都将保持不变。 - Carlo Bonamico
1
我非常感激这篇文章的作者。它拯救了我的工作。我使用angularjs构建了一个应用程序,并使用了大量的递归函数。有时候我的页面需要超过一分钟才能加载完毕。我根据这篇文章中的建议修改了代码,现在我的页面可以在5秒内加载完成。谢谢Mark Lagendijk……我不知道你是谁或者你住在哪里,但我相信你会是任何组织的资产。 - user1455719
显示剩余16条评论

25

手动添加元素并编译它们绝对是一种完美的方法。如果使用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);
    }
}
});

1
我更新了你的脚本,使其仅有一个指令。http://jsfiddle.net/KNM4q/103/ 我们如何让那个删除按钮起作用? - Benny Bottema
非常好!我已经很接近了,但是没有@position (我以为我可以在parentData[val]中找到它)。如果您使用最终版本更新您的答案 (http://jsfiddle.net/KNM4q/111/),我会接受它。 - Benny Bottema

12

我不确定这个解决方案是不是在你提供的示例中找到的或者是相同的基本概念,但我需要一个递归指令,并且我发现了一个很棒、很容易的解决方案

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 指令,然后将其包装在执行递归调用的元素周围。

1
@MarkError 和 @dnc253,这很有帮助,但是我总是收到以下错误:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree"> - Jack
1
如果其他人也遇到了这个错误,那么只有你(或Yoeman)没有重复包含任何JavaScript文件。不知何故,我的main.js文件被包含了两次,因此创建了两个具有相同名称的指令。删除其中一个JS包含后,代码就可以正常工作了。 - Jack
2
@Jack 感谢你指出这个问题。我花了好几个小时来解决这个问题,而你的评论让我朝着正确的方向前进。对于使用捆绑服务的 ASP.NET 用户,请确保在使用通配符包含捆绑时,目录中没有旧的压缩文件版本。 - Beyers
对我来说,需要将元素添加到回调函数中,例如:compiledContents(scope,function(clone) { iElement.append(clone); });。否则,"require"的控制器无法正确处理,并且会引发错误:Error: [$compile:ctreq] Controller 'tree',由指令'subTreeDirective'所需,找不到! - Tsuneo Yoshioka
我正在尝试使用AngularJS生成树形结构,但卡住了。 - Learning-Overthinker-Confused

11

从 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
谢谢您。你能把引入这个功能的变更日志链接给我吗?谢谢! - Steven
使用Angular 1.5.x非常重要。1.4.x无法工作,实际上是在jsfiddle中提供的版本。 - Paqman
在 jsfiddle https://jsfiddle.net/cattails27/5j5au76c/ 中,没有与此答案相同的代码...这是正确的吗?我错过了什么? - Paolo Biavati
小提琴显示的是Angular版本低于1.5x。 - jkris

4

经过一段时间的使用多种解决方法后,我一再回到了这个问题。

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

同样,依赖于特定模板结构的解决方案通过在指令中进行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>


2
现在 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

2

有一个非常简单的解决方法,完全不需要指令。

如果你认为你需要指令,那么从这个意义上讲,也许它甚至不是原始问题的解决方案,但如果你想要具有参数化子结构的递归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>

我发现这种结构可以在纯粹的 Angular 中像您想要的那样递归。只需遵循此设计模式,您就可以使用递归 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
} 

我唯一能看到的缺点就是你必须忍受笨拙的语法。

很抱歉,这种方法在根本上未能解决问题:使用此方法,您需要事先知道递归的深度,以便在myTemplate.html中拥有足够的控制器。 - Stewart_R
实际上,你不需要这样做。因为你的文件myTemplate.html包含了一个使用ng-include自我引用的myTemplate.html(上面的HTML内容是myTemplate.html的内容,可能没有明确说明)。这样它就变成了真正的递归。我已经在生产中使用了这种技术。 - erobwen
另外,可能没有明确说明的是,您还需要在某个地方使用ng-if来终止递归。因此,您的myTemplate.html的形式就像我在评论中更新的那样。 - erobwen

0
你可以使用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>

让它比其他解决方案更快速和简单的事情之一是“--递归”后缀。


0

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

在我看来,它比在此处找到的解决方案更基本,并且如果不是更加灵活,我们就不必使用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'和'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>

从这一点开始,如果有需要的话,控制器和模板可以被包装在自己的指令中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接