Рекурсия с 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>