【问题标题】:Recursion with ng-repeat in Angular在 Angular 中使用 ng-repeat 进行递归
【发布时间】:2016-06-13 09:15:24
【问题描述】:

在基于付费网站主题的 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 用于它自己的subItems,所以Datatable 菜单项下的子项不会被呈现。

如何创建将递归绑定到模型的视图或模板,或两者兼而有之,以便呈现所有子项的所有子项?这通常最多只有四个级别。

【问题讨论】:

  • item in item.subitems 这真的很难调试,在第二个 ng-repeat item 是第一个和第二个重复的项目,当我复制你的时,将它重命名为 subItem 或除此 Edit 之外的任何内容将代码放入 plunker 即可立即运行:plnkr.co/edit/P8zJWJO9jXtKu2Yt97TR?p=preview
  • @maurycy 我只是暂时为迭代变量保留了相同的名称,因为当我最终遇到递归绑定场景时,模板将只使用一个变量,只有一个名称,item。我没有做任何调试,模板按原样工作,除了二级子项。
  • 你检查了我的 plunk 链接吗?问题一定出在其他地方,您可以尝试在 plunk 或任何其他类似工具上重现它吗?
  • @maurycy 不,我花了 1 秒钟才发现你的 plunk 不起作用。在Datatables 菜单项下,有一个名为Managed Datatables 的子项。这不会显示在您的 plunker 中,我也不希望它显示。我的观点不是为递归而编码的,这就是这个问题的重点:我在问如何递归地编码它。

标签: javascript angularjs recursion angularjs-ng-repeat


【解决方案1】:

您可以简单地使用 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

【讨论】:

  • 更新模型时遇到问题,由于 ng-init 调用没有更新 ng-include,对此有什么想法吗?
  • 你好,@edencorbin 我的情况和你无法在 ng-include 中更新模型一样。那么你有什么解决办法吗?
【解决方案2】:

您可以简单地通过包含 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

【讨论】:

    【解决方案3】:

    如果您的意图是绘制具有不确定级别的子项的菜单,则可能一个好的实现是制作一个指令

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

    我创建了一个具有完整递归的基本示例,您可以在同一页面上看到多个菜单的简单实现,其中一个菜单上有超过 3 个级别,请参阅@ 987654321@.

    代码:

    .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。 但是,如果您要在片段中添加更多逻辑以使其可定制,对元素或 attrs (DOM) 操作进行更改,则应该使用指令。 让我对指令更满意的一点是您编写的代码的可重用性,因为在我的示例中,您可以提供更多控制并制作更通用的控制,我认为如果您的项目很大并且需要,您应该这样做成长。那么第二个问题是我的代码可以重用吗?

    提醒一下 clean 指令,您可以使用 templateUrl 代替 template,并且您可以提供一个文件来提供当前位于 template 中的 html 代码。

    如果您使用的是 1.5+,您现在可以选择使用 .component。组件是一个包装器 .direcitve,它的样板代码要少得多。在这里你可以看到不同之处。

                      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)
    

    component 的源角度指南

    编辑

    按照 Mathew Berg 的建议,如果您不想在子项列表为空的情况下包含 ul 元素,您可以将 ul 更改为类似 &lt;ul ng-if="item.subItems.length&gt;0" my-menu="item.subItems" class="sub-menu"&gt; &lt;/ul&gt;

    【讨论】:

    • 谢谢。我讨厌子项目的级别实际上变得无限,但不知道我应该处理多少,我的目标很高。今天晚些时候我会检查指令方法。
    • 很高兴听到,如果有什么事情告诉我,我会去看看。
    • 所以基本上这个指令只是给我一个递归模板的一种手段?我正在一个大型的、付费的框架中工作,其中包含其他 JS 和 Angular 代码的堆栈,所以当我在实际页面中使用它时,页面不会加载,没有错误代码。
    • 是的,如果该结构没问题,它应该可以按预期工作。如果您将它注入到 samr 页面上使用的模块中,就像您实现菜单和您正在使用的 Angular 版本一样,您可以向您展示您是如何实现该指令的名称?所以我可以尝试发现这个实现有什么问题?
    • @MathewBerg 添加了。
    【解决方案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>  
    

    【讨论】:

      【解决方案5】:

      为了在 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

      【讨论】:

        【解决方案6】:

        Rahul Arora 的回答很好,类似的例子见blog post。我要做的一个改变是使用组件而不是 ng-include。示例见Plunker

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

        【讨论】:

          【解决方案7】:

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

          希望这会有所帮助。

          【讨论】:

            【解决方案8】:

            我确定这正是您正在寻找的 -

            可以通过 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

            【讨论】:

              【解决方案9】:

              递归可能非常棘手。因为事情会失控,这取决于你的树有多深。这是我的建议:

              .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/

              如果您发现它使您的应用程序崩溃,因为您有大量列表(我很想看看),您可以在 $timeout 后面隐藏除观察者之外的 if 语句部分。

              【讨论】:

                【解决方案10】:

                你的意思是这样的吗? 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>
                

                【讨论】:

                  猜你喜欢
                  • 2014-09-14
                  • 1970-01-01
                  • 2013-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多