在Angular中使用ng-repeat的递归

19

我在一个基于付费网站主题的Angular应用程序中,我的侧边栏菜单项具有以下数据结构。该数据结构是我自己定义的,菜单从原始菜单视图派生而来,其中所有项目都是硬编码在ul中。

SidebarController.js中:

$scope.menuItems = [
    {
        "isNavItem": true,
        "href": "#/dashboard.html",
        "text": "Dashboard"
    },
    {
        "isNavItem": true,
        "href": "javascript:;",
        "text": "AngularJS Features",
        "subItems": [
            {
                "href": "#/ui_bootstrap.html",
                "text": " UI Bootstrap"
            },
            ...
        ]
    },
    {
        "isNavItem": true,
        "href": "javascript:;",
        "text": "jQuery Plugins",
        "subItems": [
            {
                "href": "#/form-tools",
                "text": " Form Tools"
            },
            {
                "isNavItem": true,
                "href": "javascript:;",
                "text": " Datatables",
                "subItems": [
                    {
                        "href": "#/datatables/managed.html",
                        "text": " Managed Datatables"
                    },
                    ...
                ]
            }
        ]
    }
];

然后我有以下的局部视图,绑定到该模型如下:

<ul class="page-sidebar-menu" data-keep-expanded="false" data-auto-scroll="true" data-slide-speed="200" ng-class="{'page-sidebar-menu-closed': settings.layout.pageSidebarClosed}">
    <li ng-repeat="item in menuItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}">
        <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
            <span class="title">{{item.text}}</span>
        </a>
        <ul ng-if="item.subItems && item.subItems.length > 0" class="sub-menu">
            <li ng-repeat="item in item.subItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}">
                <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
                    <span class="title">{{item.text}}</span>
                </a>
            </li>
        </ul>
    </li>
</ul>

注意:在视图绑定中,您可能会看不到模型中的某些$scope属性(或反之亦然),但这是因为我已经编辑过它们以减少篇幅。 现在,由于第二级li没有包含自己的有条件的ul,所以在Datatable菜单项下的子项目不会被渲染。

我该如何创建一个视图或模板,或两者兼备,以递归地绑定到模型,这样所有子项目和子项目的子项目都可以被渲染?通常只需要四个层级。


1
这可能会对你有所帮助:https://dev59.com/I5ffa4cB1Zd3GeqP3SCp#37270877 - Hadi J
3
在第二个ng-repeat内,item既是第一个循环的项又是第二个循环的项,这种情况非常难以调试,将其重命名为subItem或其他名称。[编辑]当我把你的代码复制到plunker时,它立即工作了:http://plnkr.co/edit/P8zJWJO9jXtKu2Yt97TR?p=preview - maurycy
@maurycy,我只是暂时保留了迭代变量的相同名称,因为当我最终到达递归绑定场景时,模板将仅使用一个变量,一个名称“item”。我没有进行任何调试,模板按原样工作,除了第二级子项。 - ProfK
你查看了我的 Plunk 链接吗?问题一定在其他地方,你可以试着在 Plunk 或任何类似的工具上复现它吗? - maurycy
@maurycy 不,我只用了1秒钟就看出你的plunk不起作用。在“Datatables”菜单项下,有一个名为“Managed Datatables”的子项。这在你的plunker中没有显示,我也不指望它会显示。我的观点并没有编写递归,这正是这个问题的重点:我正在询问如何递归编码。 - ProfK
我认为你可能需要一个自定义指令来很好地处理它,我会考虑结构并稍后发布(我现在正在Scrum中;) - maurycy
10个回答

16
您可以通过使用ng-include来创建一个局部视图并进行递归调用: 局部视图应该如下所示:
<ul>
    <li ng-repeat="item in item.subItems">
      <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
          <span class="title">{{item.text}}</span>
      </a>
      <div ng-switch on="item.subItems.length > 0">
        <div ng-switch-when="true">
          <div ng-init="subItems = item.subItems;" ng-include="'partialSubItems.html'"></div>  
        </div>
      </div>
    </li>
</ul>

以及您的HTML:

<ul class="page-sidebar-menu" data-keep-expanded="false" data-auto-scroll="true" data-slide-speed="200" ng-class="{'page-sidebar-menu-closed': settings.layout.pageSidebarClosed}">
    <li ng-repeat="item in menuItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}">
        <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
            <span class="title">{{item.text}}</span>
        </a>
        <ul ng-if="item.subItems && item.subItems.length > 0" class="sub-menu">

            <li ng-repeat="item in item.subItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}">
                 <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
                    <span class="title">{{item.text}}</span>
                </a>

                 <div ng-switch on="item.subItems.length > 0">
                    <div ng-switch-when="true">
                      <div ng-init="subItems = item.subItems;" ng-include="'newpartial.html'"></div>  
                    </div>
                </div>

            </li>
        </ul>
    </li>
</ul>

这是可工作的 Plunker 网址 http://plnkr.co/edit/9HJZzV4cgacK92xxQOr0?p=preview


1
更新模型时遇到问题,由于ng-init调用而无法更新ng-include,对此有什么想法吗? - edencorbin
你好,@edencorbin。我和你一样,无法在ng-include中更新模型。你有解决方案吗? - Amit Singh Rawat

11

您可以通过包含JavaScript模板并使用ng-include进行模板包含,轻松实现此目标。

定义JavaScript模板

<script type="text/ng-template" id="menu.html">...</script>

并像这样包含它:

<div ng-if="item.subItems.length" ng-include="'menu.html'"></div>

示例:在此示例中,我使用了基本的 HTML 标记,没有任何类别。您可以根据需要使用类别。我只是展示了基本的递归结构。

HTML中:

<ul>
    <li ng-repeat="item in menuItems">
      <a href="{{item.href}}">
        <span>{{item.text}}</span>
      </a>
      <div ng-if="item.subItems.length" ng-include="'menu.html'"></div>
    </li>
</ul>


<script type="text/ng-template" id="menu.html">
   <ul>
      <li ng-repeat="item in item.subItems">
        <a href="{{item.href}}">
          <span>{{item.text}}</span>
        </a>
        <div ng-if="item.subItems.length" ng-include="'menu.html'"></div>
      </li>
   </ul>
</script>

PLUNKER DEMO


7

如果您的意图是绘制具有无限级子项的菜单,则可能一个很好的实现方式是使用指令

通过指令,您将能够对您的菜单进行更多的控制。

我创建了一个基本示例,其中包含完整的递归,您可以在其中看到同一页上不止一个菜单以及一个菜单中超过3个级别的简单实现,请参见此plunker

代码:

.directive('myMenu', ['$parse', function($parse) {
    return {
      restrict: 'A',
      scope: true,
      template:
        '<li ng-repeat="item in List" ' +
        'ng-class="{\'start\': item.isStart, \'nav-item\': item.isNavItem}">' +
        '<a href="{{item.href}}" ng-class="{\'nav-link nav-toggle\': item.subItems && item.subItems.length > 0}">'+
        '<span class="title"> {{item.text}}</span> ' +
        '</a>'+
        '<ul my-menu="item.subItems" class="sub-menu"> </ul>' +
        '</li>',
      link: function(scope,element,attrs){
        //this List will be scope invariant so if you do multiple directive 
        //menus all of them wil now what list to use
        scope.List = $parse(attrs.myMenu)(scope);
      }
    }
}])

标记语言:

<ul class="page-sidebar-menu" 
    data-keep-expanded="false" 
    data-auto-scroll="true" 
    data-slide-speed="200" 
    ng-class="{'page-sidebar-menu-closed': settings.layout.pageSidebarClosed}"
    my-menu="menuItems">
</ul>

编辑

一些注释

当决定使用ng-include(我认为这是一个足够公正的解决方案)还是.directive时,你至少需要先问自己两个问题。我的代码片段是否需要一些逻辑?如果不需要,你可以选择使用ng-include。 但是,如果您将在片段中添加更多逻辑,使其可定制化,对元素或属性进行更改(DOM操作),则应选择指令。 同样,让我更放心的一点是指令的可重用性,因为在我的示例中,您可以给予更多控制并使其更加通用,我认为如果您的项目庞大并需要增长,则应该选择指令。所以第二个问题是我的代码是否会被重复使用?

干净指令的一个提醒是,您可以使用templateUrl而不是template,并且可以提供一个文件来提供当前在template中的html代码。

如果您正在使用1.5+,现在可以选择使用.component。组件是一个包装器,围绕着.directive,它具有更少的样板代码。在这里,您可以看到区别。
                  Directive                Component

bindings          No                       Yes (binds to controller)
bindToController  Yes (default: false)     No (use bindings instead)
compile function  Yes                      No
controller        Yes                      Yes (default function() {})
controllerAs      Yes (default: false)     Yes (default: $ctrl)
link functions    Yes                      No
multiElement      Yes                      No
priority          Yes                      No
require           Yes                      Yes
restrict          Yes                      No (restricted to elements only)
scope             Yes (default: false)     No (scope is always isolate)
template          Yes                      Yes, injectable
templateNamespace Yes                      No
templateUrl       Yes                      Yes, injectable
terminal          Yes                      No
transclude        Yes (default: false)     Yes (default: false)

源角指南,适用于组件

编辑

如Mathew Berg所建议的,如果您不想在子项列表为空时包含ul元素,则可以将ul更改为类似于此<ul ng-if="item.subItems.length>0" my-menu="item.subItems" class="sub-menu"> </ul>


谢谢。我不想让子项的级别变成无限的,但是不知道要处理多少个,所以我打算尽可能地高。我会在今天晚些时候检查指示方法。 - ProfK
很高兴听到这个消息,如果有什么问题,请告诉我,我会去看一下。 - Jose Rocha
基本上,这个指令只是为了给我提供一个递归模板的方式?我在一个庞大的、付费的框架中工作,其中包含大量其他的JS和Angular代码,所以当我在真实页面中使用它时,页面不会加载,并且没有任何错误代码。 - ProfK
如果该结构是正确的,那么它应该按预期工作。您能否展示一下您如何实现这个指令,以及您在哪个模块中注入了它的名称,以及您使用的 Angular 版本?这样我就可以尝试发现这个实现中的问题了。 - Jose Rocha
唯一的问题是如果没有子项,将会有一个多余的 ul dom 元素未被使用。也许可以通过根据 subItems 的长度进行条件判断来隐藏它。 - Mathew Berg
1
@MathewBerg 已添加。 - Jose Rocha

4

在审查了这些选项后,我发现这篇文章非常干净/有帮助,适用于处理模型更改的ng-include方法:http://benfoster.io/blog/angularjs-recursive-templates

总结一下:

<script type="text/ng-template" id="categoryTree">
    {{ category.title }}
    <ul ng-if="category.categories">
        <li ng-repeat="category in category.categories" ng-include="'categoryTree'">           
        </li>
    </ul>
</script>

那么

<ul>
    <li ng-repeat="category in categories" ng-include="'categoryTree'"></li>
</ul>  

1
为了在Angular中实现递归,我想使用AngularJS的基本功能,即指令。

index.html

<rec-menu menu-items="menuItems"></rec-menu>

recMenu.html

<ul>
  <li ng-repeat="item in $ctrl.menuItems">
    <a ng-href="{{item.href}}">
      <span ng-bind="item.text"></span>
    </a>
    <div ng-if="item.menuItems && item.menuItems.length">
      <rec-menu menu-items="item.menuItems"></rec-menu>
    </div>
  </li>
</ul>

recMenu.html

angular.module('myApp').component('recMenu', {
  templateUrl: 'recMenu.html',
  bindings: {
    menuItems: '<'
  }
});

这里是可用的 Plunker

0

在使用ng-include模板或编写自己的指令之前,我建议您考虑使用现有的树形组件实现。

原因是从您的描述来看,这正是您需要的。您有一个层次结构的树状数据结构需要显示。对我来说,您需要一个树形组件是显而易见的。

请查看以下实现(第一个最好):
https://github.com/angular-ui-tree/angular-ui-tree
https://github.com/wix/angular-tree-control
http://ngmodules.org/modules/angular.treeview

以上所有内容都只需要您对模型进行微小的调整,或者使用代理模型即可。

如果您坚持要自己实现它(无论您最终如何实现,本质上您仍将从头开始实现一个树形组件),我建议采用先前答案中提出的指令方法。以下是我的做法:

JS

var app=angular.module('MyApp', []);

app.controller('MyCtrl', function($scope, $window) {
  $scope.menuItems = [
    {
        "isNavItem": true,
        "href": "#/dashboard.html",
        "text": "Dashboard"
    },
    {
        "isNavItem": true,
        "href": "javascript:;",
        "text": "AngularJS Features",
        "subItems": [
            {
                "href": "#/ui_bootstrap.html",
                "text": " UI Bootstrap"
            }
        ]
    },
    {
        "isNavItem": true,
        "href": "javascript:;",
        "text": "jQuery Plugins",
        "subItems": [
            {
                "href": "#/form-tools",
                "text": " Form Tools"
            },
            {
                "isNavItem": true,
                "href": "javascript:;",
                "text": " Datatables",
                "subItems": [
                    {
                        "href": "#/datatables/managed.html",
                        "text": " Managed Datatables"
                    }
                ]
            }
        ]
    }];
});

app.directive('myMenu', ['$compile', function($compile) {
  return {
    restrict: 'E',
    scope: {
      menu: '='      
    },
    replace: true,
    link: function(scope, elem, attrs) {
      var items = $compile('<my-menu-item ng-repeat="item in menu" menu-item="item"></my-menu-item>')(scope);

      elem.append(items);
    },
    template: '<ul class="page-sidebar-menu" data-keep-expanded="false" data-auto-scroll="true" data-slide-speed="200" ng-class="{\'page-sidebar-menu-closed\': settings.layout.pageSidebarClosed}"></ul>'
  };
}]);

app.directive('myMenuItem', [function() {
  return {
    restrict: 'E',
    scope: {
      menuItem: '='
    },
    replace: true,
    template: '<li ng-class="{\'start\': item.isStart, \'nav-item\': item.isNavItem}"><a href="{{menuItem.href}}" ng-class="{\'nav-link nav-toggle\': menuItem.subItems && menuItem.subItems.length > 0}"> <span class="title">{{menuItem.text}}</span></a><my-menu menu="menuItem.subItems"></my-menu></li>'

  };
}]);

HTML

<div ng-app="MyApp" ng-controller="MyCtrl">
  <my-menu menu="menuItems"></my-menu>
</div>

这里有一个可工作的CodePen示例:http://codepen.io/eitanfar/pen/oxZrpQ

一些注意事项

  1. 你不必使用两个指令(“my-menu”,“my-menu-item”),你可以只使用一个(简单地用“my-menu-item”的模板替换它的ng-repeat),但是我认为这样更连贯。
  2. 你尝试的指令解决方案没有起作用的原因(我猜测,因为我没有调试过你的尝试),是因为它陷入了无限循环。它之所以会这样做,是因为链接首先发生在内部元素上。我在我的建议解决方案中所做的是,将子项的链接推迟到父菜单的链接完成后再进行。这可能会带来任何缺点都可以通过在作用域中提供引用(如我提供的'menuItem'绑定)来克服。

希望这能帮到你。


0

我相信这正是你在寻找的 -

你可以通过ng-repeat实现无限递归。

<script type="text/ng-template"  id="tree_item_renderer.html">
{{data.name}}
<button ng-click="add(data)">Add node</button>
<button ng-click="delete(data)" ng-show="data.nodes.length > 0">Delete nodes</button>
<ul>
    <li ng-repeat="data in data.nodes" ng-include="'tree_item_renderer.html'"></li>
</ul>

  angular.module("myApp", []).
controller("TreeController", ['$scope', function($scope) {
    $scope.delete = function(data) {
        data.nodes = [];
    };
    $scope.add = function(data) {
        var post = data.nodes.length + 1;
        var newName = data.name + '-' + post;
        data.nodes.push({name: newName,nodes: []});
    };
    $scope.tree = [{name: "Node", nodes: []}];
}]);

这里是jsfiddle

0

递归可能非常棘手。根据您的树有多深,情况会失控。这是我的建议:

.directive('menuItem', function($compile){
    return {
        restrict: 'A',
        scope: {
            menuItem: '=menuItem'
        },
        templateUrl: 'menuItem.html',
        replace: true,
        link: function(scope, element){
            var watcher = scope.$watch('menuItem.subItems', function(){
                if(scope.menuItem.subItems && scope.menuItem.subItems.length){
                    var subMenuItems = angular.element('<ul><li ng-repeat="subItem in menuItem.subItems" menu-item="subItem"></li></ul>')
                    $compile(subMenuItems)(scope);
                    element.append(subMenuItems);
                    watcher();
                }
            });
        }           
    }
})

HTML:

<li>    
    <a ng-href="{{ menuItem.href }}">{{ menuItem.text }}</a>
</li>

这将确保它不会重复创建子项。您可以在此处查看jsFiddle中的工作方式:http://jsfiddle.net/gnk8vcrv/

如果您发现它因为有大量列表而导致应用程序崩溃(我很想看到),您可以将if语句中watcher后面的部分隐藏在$timeout后面。


0

Rahul Arora的回答很好,可以看看这个博客文章,里面有类似的例子。我会做一个改变,使用组件而不是ng-include。例如请看这个Plunker

app
  .component('recursiveItem', {
    bindings: {
      item: '<'
    },
    controllerAs: 'vm',
    templateUrl: 'newpartial.html'
  });

-1
你是不是想要这样的效果?http://jsfiddle.net/uXbn6/3639/

JS

angular.module("myApp", []).controller("TreeController", ['$scope',function($scope) {


    $scope.menuItems = [{
      "isNavItem": true,
      "href": "#/dashboard.html",
      "text": "Dashboard"
    }, {
      "isNavItem": true,
      "href": "javascript:;",
      "text": "AngularJS Features",
      "subItems": [{
        "href": "#/ui_bootstrap.html",
        "text": " UI Bootstrap"
      }, {
        "isNavItem": true,
        "href": "javascript:;",
        "text": "AngularJS Features",
        "subItems": [{
          "href": "#/ui_bootstrap.html",
          "text": " UI Bootstrap"
        }]
      }]
    }, {
      "isNavItem": true,
      "href": "javascript:;",
      "text": "jQuery Plugins",
      "subItems": [{
        "href": "#/form-tools",
        "text": " Form Tools"
      }, {
        "isNavItem": true,
        "href": "javascript:;",
        "text": " Datatables",
        "subItems": [{
          "href": "#/datatables/managed.html",
          "text": " Managed Datatables"
        }]
      }]
    }];
  }]);

HTML

  <script type="text/ng-template" id="tree_item_renderer.html">
    <a href="{{item.href}}" ng-class="{'nav-link nav-toggle': item.subItems && item.subItems.length > 0}">
      <span class="title">{{item.text}}</span>
    </a>
    <ul ng-if="item.subItems && item.subItems.length > 0" class="sub-menu">
      <li ng-repeat="item in item.subItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}" ng-include="'tree_item_renderer.html'"></li>
    </ul>
  </script>

  <div ng-app="myApp" ng-controller="TreeController">
    <ul class="page-sidebar-menu" data-keep-expanded="false" data-auto-scroll="true" data-slide-speed="200" ng-class="{'page-sidebar-menu-closed': settings.layout.pageSidebarClosed}">
      <li ng-repeat="item in menuItems" ng-class="{'start': item.isStart, 'nav-item': item.isNavItem}" ng-include="'tree_item_renderer.html'"></li>
    </ul>
  </div>

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