Angular 指令中的递归

IT技术 javascript recursion angularjs
2021-01-28 20:31:05

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

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

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

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

6个回答

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

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

  1. 你不需要一个特殊的指令来让你的 html 不那么干净。
  2. 递归逻辑被抽象到 RecursionHelper 服务中,因此您可以保持指令干净。

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

最初的问题是,当您使用递归指令时,AngularJS 会陷入无限循环。这段代码通过在指令的编译事件期间移除内容,并在指令的链接事件中编译和重新添加内容来打破这个循环。
2021-03-23 20:31:05
谢谢,很好的解决方案!真的很干净并且开箱即用,让我在两个相互包含的指令之间进行递归工作。
2021-03-25 20:31:05
在您的示例中,您可以替换compile: function(element) { return RecursionHelper.compile(element); }compile: RecursionHelper.compile.
2021-03-29 20:31:05
这是优雅的,如果/当 Angular 核心实现类似的支持时,您只需删除自定义编译包装器,所有剩余的代码将保持不变。
2021-04-01 20:31:05
如果您希望模板位于外部文件中怎么办?
2021-04-05 20:31:05

手动添加元素并编译它们绝对是一种完美的方法。如果您使用 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我们如何使删除按钮起作用?
2021-03-18 20:31:05
非常好!我非常接近,但没有@position(我以为我可以用 parentData[val] 找到它。如果你用最终版本(jsfiddle.net/KNM4q/111更新你的答案,我会接受它。
2021-03-19 20:31:05

我不确定是否在您链接的示例之一或相同的基本概念中找到此解决方案,但我需要一个递归指令,并且我找到了一个很好的、简单的解决方案

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

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

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

谢谢你。你能把我链接到介绍这个功能的更新日志吗?谢谢!
2021-03-11 20:31:05
使用 angular 1.5.x 非常重要。1.4.x 无法工作,实际上是 jsfiddle 中提供的版本。
2021-03-24 20:31:05
在 jsfiddle jsfiddle.net/cattails27/5j5au76c 中 ,这个答案的代码不一样......是吗?我错过了什么?
2021-04-07 20:31:05
小提琴显示小于 1.5 倍的角度版本
2021-04-08 20:31:05

在使用了一段时间的解决方法之后,我反复回到这个问题。

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

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