web-dev-qa-db-fra.com

Récursion avec ng-repeat dans Angular

J'ai la structure de données suivante pour les éléments dans mon menu sidem, dans une application Angular basée sur un thème de site Web payant. La structure de données est la mienne et le menu est dérivé de la vue de menu d'origine avec tous les éléments de la variable ul codés en dur.

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

Ensuite, j'ai la vue partielle suivante liée à ce modèle comme suit:

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

NOTEIl peut y avoir des propriétés $scope dans les liaisons de vue que vous ne voyez pas dans le modèle, ou inversement, mais c'est parce que je les ai modifiées par souci de concision. Maintenant, comme le second niveau li ne contient pas également une ul conditionnelle pour sa propre subItems, les sous-éléments de l'élément de menu Datatable ne sont pas rendus.

Comment puis-je créer une vue ou un modèle, ou les deux, qui seront liés de manière récursive au modèle, de sorte que tous les sous-éléments de tous les sous-éléments soient rendus? Ce ne sera normalement que jusqu'à quatre niveaux.

17
ProfK

Vous pouvez simplement utiliser ng-include pour créer un partiel et l'appeler de manière récursive: Partial devrait ressembler à ceci:

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

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

Voici le travail de travail http://plnkr.co/edit/9HJZzV4cgacK92xxQOr0?p=preview

15
Rahul Arora

Vous pouvez y parvenir simplement en incluant un modèle javascript et un modèle include en utilisant ng-include 

définir un modèle javascript

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

et l'inclure comme:

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

Exemple: Dans cet exemple, j'ai utilisé le langage HTML de base sans classe. Vous pouvez utiliser les classes à votre guise. Je viens de montrer la structure de base de la récursivité.

Dans 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

Si votre intention est de dessiner un menu avec un nombre indéfini de sous-éléments, une bonne implémentation consiste probablement à créer une directive .

Avec une directive, vous serez en mesure d’assumer plus de contrôle sur votre menu.

J'ai créé un exemple basique complet avec récursion, avec lequel vous pouvez voir une implémentation facile de plusieurs menus sur la même page et de plus de 3 niveaux sur l'un des menus, voir ceci 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);
      }
    }
}])

Balisage:

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

Modifier

Quelques notes 

Quand il s'agit de prendre une décision concernant ng-include (je pense que c'est une solution assez juste) ou .directive, vous devez vous poser au moins deux questions. Mon fragment de code va-t-il avoir besoin de logique? Sinon, vous pouvez également vous contenter de ng-include . Mais si vous voulez mettre plus de logique dans le fragment afin de le rendre personnalisable, apportez des modifications à la manipulation de l'élément ou attr (DOM), vous devriez opter pour la directive . Un autre point qui me rend plus à l'aise avec la directive est la réutilisabilité du code que vous écrivez, car dans mon exemple, vous pouvez donner plus de contrôle à un contrôleur et en créer un plus générique, je suppose que vous devriez y aller si votre projet est grand et doit grandir. Donc, la deuxième question est-ce que mon code va être réutilisable?

Un rappel pour une directive de nettoyage est qu'au lieu de template, vous pouvez utiliser templateUrl et vous pouvez donner un fichier pour alimenter le code html actuellement présent dans template.

Si vous utilisez 1.5+, vous pouvez maintenant choisir d'utiliser .component. Le composant est un emballage .direcitve qui contient beaucoup moins de code standard. Ici vous pouvez voir la différence.

                  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)

Guide angulaire source pour composant

Modifier

Comme suggéré par Mathew Berg, si vous souhaitez ne pas inclure l'élément ul si la liste des sous-éléments est vide, vous pouvez modifier le paramètre ul pour qu'il ressemble à ceci <ul ng-if="item.subItems.length>0" my-menu="item.subItems" class="sub-menu"> </ul>

7
Jose Rocha

Après avoir examiné ces options, j’ai trouvé cet article très propre/utile pour une approche ng-include qui gère bien les modifications de modèle: http://benfoster.io/blog/angularjs-recursive-templates

En résumé:

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

puis

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

Avant d’utiliser des modèles avec ng-include ou d’écrire votre propre directive, je vous suggère d’envisager d’utiliser une implémentation de composant d’arbre existante.
La raison en est que d'après votre description, c'est exactement ce dont vous avez besoin. Vous souhaitez afficher une structure de données arborescente hiérarchique. Il me semble évident que vous avez besoin d'un composant d'arbre.

Jetez un coup d'œil aux implémentations suivantes (la première préférable):
https://github.com/angular-ui-tree/angular-ui-tree
https://github.com/wix/angular-tree-control
http://ngmodules.org/modules/angular.treeview

Tout ce qui précède nécessite seulement que vous apportiez une modification mineure à votre modèle ou que vous utilisiez un modèle de proxy.

Si vous insistez pour le mettre en œuvre vous-même (et quelle que soit la façon dont vous le réalisez, vous allez toujours implémenter une composante arborescente à partir de rien), je suggérerais l'approche directive, telle que proposée dans les réponses précédentes. Voici comment je le ferais:

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>

Voici un exemple de travail avec CodePen: http://codepen.io/eitanfar/pen/oxZrpQ

Quelques notes

  1. Vous ne devez pas utiliser 2 directives ("my-menu", "my-menu-item"), vous pouvez en utiliser seulement 1 (il suffit de remplacer le ng-repeat de "my-menu-item" par son modèle), cependant, je pense que c'est plus cohérent de cette façon
  2. La raison pour laquelle la solution de directive que vous avez essayée n'a pas fonctionné (une supposition éclairée, car je n'ai pas débogué votre tentative), c'est qu'elle se trouve dans une boucle infinie. C'est le cas, car la liaison se produit d'abord pour les éléments internes. Ce que je fais dans la solution proposée est de reporter la liaison des sous-éléments jusqu’à ce que la liaison du menu parent soit terminée. Les inconvénients que cela peut avoir peuvent être surmontés en fournissant des références dans la portée (comme je fournis la liaison 'menuItem').

J'espère que cela t'aides.

0
eitanfar

Je suis sûr que c'est exactement ce que vous recherchez -

Vous pouvez obtenir une récursion illimitée avec 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: []}];
}]);

Voici jsfiddle

0
Abhinav

La récursion peut être très délicate. Comme les choses vont devenir incontrôlables en fonction de la profondeur de votre arbre. Voici ma suggestion:

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

Cela garantira qu'il ne crée pas de sous-éléments à plusieurs reprises. Vous pouvez le voir fonctionner dans un jsFiddle ici: http://jsfiddle.net/gnk8vcrv/

Si vous rencontrez le blocage de votre application parce que vous avez un grand nombre de listes (je serais intéressé de le voir), vous pouvez masquer les parties de l'instruction if à côté de l'observateur derrière un délai d'attente de $.

0
Mathew Berg

Afin de faire de la récursion en angulaire, j'aimerais utiliser les fonctionnalités de base de angularJS et i.e directive.

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

Ici travaille Plunker

0
varit05

La réponse de Rahul Arora est bonne, voir cet article blog pour un exemple similaire. Le seul changement que je ferais serait d'utiliser un composant au lieu de ng-include. Pour un exemple, voyez ceci Plunker :

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