Рекурсия с ng-repeat в Angular

У меня есть следующая структура данных для элементов в моем sidemenu, в приложении 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 не отображаются.

Как я могу создать представление или шаблон или оба, которые будут рекурсивно привязываться к модели, чтобы отображались все подпозиции всех подпозиций? Обычно это может быть до четырех уровней.

Ответы

Ответ 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>

Вот рабочий плункер http://plnkr.co/edit/9HJZzV4cgacK92xxQOr0?p=preview

Ответ 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

Если вы намереваетесь нарисовать меню с неопределенным уровнем подпунктов, возможно, хорошей реализацией является создание директивы .

С директивой вы сможете больше контролировать свой меню.

Я создал базовый exepmle с полной рекурсией, с помощью которой вы можете легко просмотреть более одного меню на одной странице и более трех уровней в одном из меню, см. это 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>

Edit

Некоторые заметки

Когда дело доходит до принятия решения о ng-include (которое, я думаю, достаточно справедливое решение) или .directive, сначала вы должны задать себе как минимум два вопроса. Мне нужен фрагмент кода? Если бы вы не могли, просто пойти на ng-include. Но если вы собираетесь добавить больше логики в фрагмент, чтобы сделать его настраиваемым, внесите изменения в манипуляции с элементами или attrs (DOM), вы должны пойти на директиву. Также один момент, который заставляет меня упасть более комфортно с директивой, - это повторное использование кода, который вы пишете, поскольку в моем примере вы можете дать controll больше и сделать более общий, я предполагаю, что вы должны пойти на это, если ваш проект большой и нуждается в расти. Итак, второй вопрос: мой код будет повторно использоваться?

Напоминание о чистой директиве заключается в том, что вместо template вы можете использовать templateUrl, и вы можете предоставить файл для подачи кода html, который в настоящее время находится в template.

Если вы используете 1.5+, вы можете теперь использовать .component. Компонент - это оболочка arroud .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)

Справочник источника angular для компонент

Edit

Как сказано Мэтью Бергом, если вы не хотите включать элемент ul, если список subItems пуст, вы можете изменить ul на это примерно так: <ul ng-if="item.subItems.length>0" my-menu="item.subItems" class="sub-menu"> </ul>

Ответ 4

Ответ Рахула Ароры хорош, см. этот пост в блоге для аналогичного примера. Единственное изменение, которое я бы сделал, это использовать компонент вместо ng-include. Пример см. В этом Plunker:

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

Ответ 5

Чтобы сделать рекурсию в Angular, я хотел бы использовать основную функцию angularJS и i.e директивы.

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

Прежде чем использовать шаблоны с ng-include или написать свою собственную директиву, я бы предложил рассмотреть возможность использования существующей реализации древовидного компонента. Причина в том, что из вашего описания это именно то, что вам нужно. У вас есть иерархическая древовидная структура данных, которую вы хотите отобразить. Для меня кажется очевидным, что вам нужен компонент дерева.

Взгляните на следующие реализации (1-й вариант предпочтительнее):
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

Некоторые примечания

  • Вам не нужно использовать 2 директивы ( "my-menu", "my-menu-item" ), вы можете использовать только 1 (просто замените ng-repeat "my-menu-item" своим шаблон), однако, я думаю, что он более согласован таким образом.
  • Причина, по которой решение, которое вы пробовали, не сработало (просвещенная догадка, поскольку я не отлаживал вашу попытку), заключается в том, что он запускается в бесконечный цикл. Он делает это, поскольку привязка происходит сначала для внутренних элементов. То, что я делаю в своем предлагаемом решении, заключается в том, чтобы отложить привязку вспомогательных элементов до завершения компоновки родительского меню. Любые недостатки, которые это может иметь, могут быть устранены путем предоставления ссылок в области (поскольку я предоставляю привязку "menuItem" ).

Надеюсь, что это поможет.

Ответ 7

Я уверен, что это именно то, что вы ищете -

Вы можете добиться неограниченной рекурсии с помощью 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

Ответ 8

Рекурсия может быть очень сложной. Поскольку вещи выйдут из-под контроля в зависимости от того, насколько глубоко ваше дерево. Вот мое предложение:

.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, кроме наблюдателя за $timeout.

Ответ 9

После рассмотрения этих опций я нашел эту статью очень чистой/полезной для подхода 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>  

Ответ 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>