Angular指令中的递归


178

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

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

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

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

Answers:


315

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

看到这个柱塞的演示。我最喜欢此解决方案,因为:

  1. 您不需要特殊的指令,这会使您的html不太干净。
  2. 递归逻辑被抽象到RecursionHelper服务中,因此您可以保持指令整洁。

更新:从Angular 1.5.x开始,不再需要其他技巧,但仅适用于template,不适用于templateUrl


3
谢谢,很好的解决方案!真的很干净,我可以立即使用,在包含彼此工作的两个指令之间进行递归。
jssebastian

6
最初的问题是,当您使用递归指令时,AngularJS会陷入无限循环。该代码通过在指令的compile事件期间删​​除内容,并在指令的link事件中编译并重新添加内容来打破此循环。
Mark Lagendijk 2013年

15
在您的示例中,您可以替换compile: function(element) { return RecursionHelper.compile(element); }compile: RecursionHelper.compile
Paolo Moretti 2014年

1
如果您希望模板位于外部文件中怎么办?
CodyBugstein 2015年

2
如果/当Angular核心实现了类似的支持时,您可以删除自定义编译包装器,而所有其余代码将保持不变,就此而言,这是很优雅的。
卡洛·波纳米科

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
我更新了您的脚本,使其只有一个指令。jsfiddle.net/KNM4q/103如何使删除按钮起作用?
Benny Bottema

非常好!我非常接近,但没有@position(我想我可以用parentData [VAL]找到它,如果你与最终版本更新你的答案(jsfiddle.net/KNM4q/111)我会接受它。
Benny Bottema 2013年

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">
杰克

1
如果其他任何人遇到此错误,则仅您(或Yoeman)没有包含任何JavaScript文件一次。不知何故我的main.js文件被包含两次,因此创建了两个具有相同名称的指令。删除其中一个JS包含文件后,代码即可正常工作。
杰克

2
@Jack感谢您指出这一点。只需花费几个小时就可以解决此问题,您的意见将我引向了正确的方向。对于使用捆绑服务的ASP.NET用户,在使用捆绑中的通配符时,请确保目录中没有旧的缩小版本的文件。
Beyers 2013年

对我来说,是必要元素里面添加回调,如:compiledContents(scope,function(clone) { iElement.append(clone); });。否则,“规定”编辑控制器不正确处理,和错误:Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!原因。
2015年

我正在尝试使用angular js生成树结构,但仍受其困扰。
Learning-Overthinker-Confused

10

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


1
谢谢你 您能否将我链接到引入此功能的变更日志?谢谢!
史蒂文(Steven)

使用角度1.5.x非常重要。1.4.x不起作用,实际上是jsfiddle中提供的版本。
Paqman'9

在jsfiddle jsfiddle.net/cattails27/5j5au76c中 ,没有与此答案相同的代码...对吗?我想念的是什么?
Paolo Biavati

小提琴显示角度版本小于1.5倍
jkris '17

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参数化子结构的递归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
} 

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


恐怕这无法从根本上解决问题:使用这种方法,您将需要预先知道递归的深度,以便在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

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

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>

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.