web-dev-qa-db-ger.com

Rekursion mit ng-repeat in Angular

Ich habe die folgende Datenstruktur für Elemente in meinem Seitenmenü in einer Angular App, die auf einem kostenpflichtigen Website-Design basiert. Die Datenstruktur ist meine eigene, und das Menü wird aus der ursprünglichen Menüansicht mit allen Elementen in der ul hardcodiert abgeleitet.

In 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"
                    },
                    ...
                ]
            }
        ]
    }
];

Dann habe ich die folgende Teilansicht wie folgt an dieses Modell gebunden:

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

NOTEEs gibt möglicherweise $scope -Eigenschaften in den Ansichtsbindungen, die Sie im Modell nicht sehen, oder umgekehrt, aber das liegt daran, dass ich sie der Kürze halber bearbeitet habe. Da die li der zweiten Ebene keine bedingte ul für ihre eigene subItems enthält, werden die Unterelemente unter dem Menüelement Datatable nicht gerendert.

Wie kann ich eine Ansicht oder eine Vorlage oder beides erstellen, die rekursiv an das Modell gebunden wird, sodass alle Unterelemente aller Unterelemente gerendert werden? Dies sind normalerweise nur bis zu vier Stufen.

17
ProfK

Sie können einfach ng-include verwenden, um ein Partial zu erstellen und es rekursiv aufzurufen: Partial sollte ungefähr so ​​aussehen:

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

Und dein 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>

Hier ist der funktionierende Plunker http://plnkr.co/edit/9HJZzV4cgacK92xxQOr0?p=preview

15
Rahul Arora

Sie können dies einfach erreichen, indem Sie eine javascript-Vorlage und ein Vorlagen-Include mit ng-include einfügen. 

javascript-Vorlage definieren

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

und füge es ein wie:

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

Example: In diesem Beispiel habe ich grundlegendes HTML ohne Klassen verwendet. Sie können Klassen nach Bedarf verwenden. Ich habe gerade die grundlegende Rekursionsstruktur gezeigt.

In 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

9
Shaishab Roy

Wenn Sie ein Menü mit einer unbestimmten Anzahl von Unterelementen zeichnen möchten, ist es wahrscheinlich eine gute Implementierung, eine -Direktive zu erstellen.

Mit einer Anweisung können Sie mehr Kontrolle über Ihr Menü übernehmen.

Ich habe ein grundlegendes Beispiel mit vollständiger Rekursion erstellt, bei dem Sie eine einfache Implementierung von mehr als einem Menü auf derselben Seite und mehr als drei Ebenen sehen können eines der Menüs finden Sie in diesem Plunker .

Code:

.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);
      }
    }
}])

Markup:

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

Bearbeiten

Einige Notizen

Wenn es darum geht, die Entscheidung über ng-include (Das halte ich für eine faire Lösung) oder .directive Zu treffen, müssen Sie sich zuerst mindestens zwei Fragen stellen. Benötigt mein Codefragment eine Logik? Wenn nicht, können Sie sich auch für das ng-include entscheiden. Wenn Sie jedoch mehr Logik in das Fragment einfügen möchten, um es anpassbar zu machen, Änderungen an den Elementen oder der DOM-Manipulation (attrs) vornehmen möchten, sollten Sie die Direktive wählen. Außerdem ist ein Punkt, der mich mit der Direktive einverstanden macht, die Wiederverwendbarkeit des von Ihnen geschriebenen Codes, da Sie in meinem Beispiel mehr Kontrolle geben und einen allgemeineren Code erstellen können. Ich gehe davon aus, dass Sie dies tun sollten, wenn Ihr Projekt groß ist und Bedürfnisse hat wachsen. Die zweite Frage ist also, ob mein Code wiederverwendbar ist.

Eine Erinnerung für eine saubere Direktive ist, dass Sie anstelle von template das templateUrl verwenden und eine Datei angeben können, um den HTML-Code einzugeben, der sich derzeit im template befindet.

Wenn Sie 1.5+ verwenden, können Sie jetzt .component Auswählen. Komponente ist ein Wrapper-Arroud .direcitve, Der viel weniger Boilerplate-Code enthält. Hier sehen Sie den Unterschied.

                  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)

Quelle angular Anleitung für Komponente

Bearbeiten

Wie von Mathew Berg vorgeschlagen, wenn Sie das ul-Element nicht einschließen möchten, wenn die Liste der Unterelemente leer ist, können Sie die ul in so etwas wie dieses ändern <ul ng-if="item.subItems.length>0" my-menu="item.subItems" class="sub-menu"> </ul>

7
Jose Rocha

Nachdem ich diese Optionen durchgesehen hatte, fand ich diesen Artikel für einen ng-include-Ansatz, der mit Modelländerungen gut umgeht, sehr sauber/hilfreich: http://benfoster.io/blog/angularjs-recursive-templates

In Summe:

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

dann

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

Bevor Sie Vorlagen mit ng-include verwenden oder Ihre eigene Direktive schreiben, empfiehlt es sich, eine vorhandene Implementierung der Baumkomponente in Betracht zu ziehen.
Der Grund ist, dass Sie aus Ihrer Beschreibung genau das benötigen, was Sie brauchen. Sie haben eine hierarchische Baumstruktur, die Sie anzeigen möchten. Es scheint mir offensichtlich, dass Sie eine Baumkomponente benötigen.

Sehen Sie sich die folgenden Implementierungen an (1. bevorzugt):
https://github.com/angular-ui-tree/angular-ui-tree
https://github.com/wix/angular-tree-control
http://ngmodules.org/modules/angular.treeview

Für alle oben genannten Punkte müssen Sie lediglich eine geringfügige Anpassung an Ihrem Modell vornehmen oder alternativ ein Proxy-Modell verwenden.

Wenn Sie darauf bestehen, es selbst zu implementieren (und unabhängig davon, wie Sie es am Ende tun werden, werden Sie im Wesentlichen immer noch eine Baumkomponente von Grund auf implementieren), würde ich den in den vorherigen Antworten vorgeschlagenen Richtlinienansatz vorschlagen. So würde ich es machen:

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>

Hier ist ein funktionierendes CodePen-Beispiel: http://codepen.io/eitanfar/pen/oxZrpQ

Einige Notizen

  1. Sie müssen keine 2 Direktiven verwenden ("Mein Menü", "Mein Menü"), Sie können nur 1 verwenden (ersetzen Sie einfach die Wiederholwiederholung von "Mein Menü" durch ihre Vorlage), Ich denke jedoch, dass es auf diese Weise kohärenter ist
  2. Der Grund, warum die von Ihnen ausgeführte Direktive-Lösung nicht funktioniert hat (eine fundierte Vermutung, da ich Ihren Versuch nicht debugiert habe), liegt darin, dass sie in eine Endlosschleife gerät. Dies geschieht, da das Verknüpfen zuerst für interne Elemente erfolgt. Was ich in meiner vorgeschlagenen Lösung mache, ist das Verschieben der Verknüpfung der Unterelemente bis nach Abschluss der Verknüpfung des übergeordneten Menüs. Mögliche Nachteile können durch die Angabe von Referenzen im Geltungsbereich (da ich die 'menuItem'-Bindung bereitstelle) behoben werden.

Hoffe das hilft.

0
eitanfar

Ich bin sicher, genau das, was Sie suchen -

Sie können eine unbegrenzte Rekursion durch ng-repeat erreichen

<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: []}];
}]);

Hier ist jsfiddle

0
Abhinav

Rekursion kann sehr schwierig sein. Da die Dinge aus dem Ruder laufen, je nachdem wie tief Ihr Baum ist. Hier ist mein Vorschlag:

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

Dadurch wird sichergestellt, dass keine Unterelemente mehrmals erstellt werden. Sie können es in einem jsFiddle-System hier sehen: http://jsfiddle.net/gnk8vcrv/

Wenn Sie feststellen, dass Ihre App abstürzt, weil Sie sehr viele Listen haben (ich würde gerne sehen), können Sie die Teile der if-Anweisung neben dem Watcher hinter einem $ timeout ausblenden.

0
Mathew Berg

Um eine Rekursion in Angular durchzuführen, würde ich gerne die grundlegende Funktion von angleJS und die Direktive verwenden.

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: '<'
  }
});

Hier arbeitet Plunker

0
varit05

Rahul Aroras Antwort ist gut, siehe diesen Blogbeitrag für ein ähnliches Beispiel. Die einzige Änderung, die ich vornehmen würde, ist die Verwendung einer Komponente anstelle von ng-include. Für ein Beispiel siehe Plunker :

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