Angular 指令中的递归

Posted

技术标签:

【中文标题】Angular 指令中的递归【英文标题】:Recursion in Angular directives 【发布时间】:2013-01-04 00:52:28 【问题描述】:

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

根据运行时范围状态手动增量“编译”html example 1 [***] example 2 [angular jsfiddles page] 根本不使用指令,而是使用引用自身的

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

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

有没有一种好方法可以使用参数化的递归模式来管理添加/删除元素以反映运行时状态?也就是说,具有添加/删除节点按钮和一些输入字段的树,其值向下传递到节点的子节点。也许是第二种方法与链式作用域的结合(但我不知道该怎么做)?

【问题讨论】:

【参考方案1】:

受@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 开始,不再需要任何技巧,但仅适用于 template,不适用于 templateUrl

【讨论】:

谢谢,很好的解决方案!真的很干净,开箱即用,让我在两个相互包含的指令之间进行递归。 原来的问题是当你使用递归指令时,AngularJS 会陷入死循环。此代码通过在指令的编译事件期间删​​除内容,并在指令的链接事件中编译并重新添加内容来打破此循环。 在您的示例中,您可以将 compile: function(element) return RecursionHelper.compile(element); 替换为 compile: RecursionHelper.compile 如果您希望模板位于外部文件中怎么办? 这很优雅,如果/当 Angular 核心实现类似的支持时,您可以删除自定义编译包装器,所有剩余代码都将保持不变。【参考方案2】:

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

);

【讨论】:

我更新了你的脚本,使它只有一个指令。 jsfiddle.net/KNM4q/103我们怎样才能使删除按钮起作用? 非常好!我非常接近,但没有@position(我想我可以用 parentData[val] 找到它。如果你用最终版本(jsfiddle.net/KNM4q/111)更新你的答案,我会接受它。【参考方案3】:

我不确定是否在您链接的示例之一或相同的基本概念中找到了此解决方案,但我需要一个递归指令,我找到了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 指令,然后将其包裹在进行递归调用的元素周围。

【讨论】:

@MarkError 和 @dnc253 这很有帮助,但是我总是收到以下错误:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: &lt;recursive tree="tree"&gt; 如果其他人遇到此错误,只有您(或 Yoeman)没有多次包含任何 javascript 文件。不知何故,我的 main.js 文件被包含了两次,因此创建了两个同名的指令。删除其中一个 JS 包含后,代码工作。 @Jack 感谢您指出这一点。只需花费几个小时来解决此问题,您的评论就为我指明了正确的方向。对于使用捆绑服务的 ASP.NET 用户,在捆绑中使用通配符包含时,请确保目录中没有旧的缩小版本的文件。 对我来说,需要在回调中添加元素,例如:compiledContents(scope,function(clone) iElement.append(clone); );。否则,“require”ed 控制器未正确处理,错误:Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found! 原因。 我正在尝试使用 Angular js 生成树形结构,但还是坚持了下来。【参考方案4】:

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

【讨论】:

谢谢。您能否将我链接到引入此功能的更改日志?谢谢! 使用 angular 1.5.x 非常重要。 1.4.x 不行,实际上是jsfiddle中提供的版本。 在 jsfiddle jsfiddle.net/cattails27/5j5au76c 中,这个答案的代码不一样......对吗?我错过了什么? 小提琴显示小于 1.5x 的角度版本【参考方案5】:

在使用了一段时间后,我反复回到这个问题。

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

同样,通过在指令中进行 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>

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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

好吧,从这个意义上说,如果您假设您需要指令,它甚至不是原始问题的解决方案,但如果您想要一个具有 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>

我很巧合地发现,这种结构可以按照你喜欢的方式在普通的原版 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 中有足够的控制器 实际上,你没有。由于您的文件 myTemplate.html 包含使用 ng-include 对 myTemplate.html 的自我引用(上面的 html 内容是 myTemplate.html 的内容,可能没有明确说明)。这样它就变成了真正的递归。我已经在生产中使用了该技术。 另外,也许没有明确说明您还需要在某处使用 ng-if 来终止递归。所以你的 myTemplate.html 就是我评论中更新的形式。【参考方案8】:

您可以为此使用 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”后缀。

【讨论】:

【参考方案9】:

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

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”和“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>

从现在开始,如果需要,控制器和模板可以包装在它自己的指令中。

【讨论】:

以上是关于Angular 指令中的递归的主要内容,如果未能解决你的问题,请参考以下文章

TS中的Angular 2访问指令返回未定义

尝试从 Angular 5 中的 JSON 递归获取键值

Angular.js中的指令——易懂全解析

一“括”抵千言 —— Angular 2中的绑定

Angular:在搜索栏中的用户类型之前阻止显示指令

@Component错误中的Angular2版本RC.6“指令”