Est-il possible de créer une arborescence avec Angular?

177

Je cherche à afficher des données dans une structure arborescente dans une application Web. J'espérais utiliser Angular pour cette tâche.

On dirait que ng-repeat me permettra d'itérer dans une liste de nœuds, mais comment puis-je faire une imbrication lorsque la profondeur d'un nœud donné augmente?

J'ai essayé le code suivant , mais l'échappement automatique du HTML l'empêche de fonctionner. De plus, la balise end ul est au mauvais endroit.

Je suis à peu près sûr que j'aborde ce problème dans le mauvais sens.

Des idées?

Jon Abrams
la source
Je viens de répondre de manière assez générique sur une question différente: stackoverflow.com/questions/14430655/…
tilgovi

Réponses:

231

Jetez un œil à ce violon

Original: http://jsfiddle.net/brendanowen/uXbn6/8/

Mis à jour: http://jsfiddle.net/animaxf/uXbn6/4779/

Cela devrait vous donner une bonne idée de la façon d'afficher un en tree like structureutilisant angular. C'est une sorte d'utiliser la récursivité en html!

ganaraj
la source
94
pourquoi ne pas indiquer votre source ? vous avez écrit un message dans ce fil, et maintenant vous publiez une URL ici avec votre propre nom?
Janus Troelsen
5
Voici une version identique (je pense), sauf qu'elle se charge beaucoup plus rapidement (pour moi du moins), car elle n'a pas Twitter Bootstrap en ligne dans la section CSS. jsfiddle.net/brendanowen/uXbn6/8
KajMagnus
10
mec, vous devriez indiquer votre source.
Ajax3.14
46
J'étais vraiment fatigué que les gens disent constamment que l'URL contient mon nom (et donc c'est du plagiat!) C'est malheureusement ainsi que fonctionne jsfiddle. Si vous payez quelque chose pendant que vous êtes connecté, votre nom d'utilisateur est conservé. Cela dit, j'ai maintenant lié à l'URL d'origine. Contester une réponse si elle est fausse - La réponse se trouve être correcte dans ce scénario avec la seule chose que l'URL de sauvegarde que j'avais semble contenir mon nom.
ganaraj le
5
Je viens d'ajouter le bouton Réduire et Développer à votre version: jsfiddle.net/uXbn6/639
jbaylina
77

Si vous utilisez Bootstrap CSS ...

J'ai créé un simple contrôle d'arborescence réutilisable (directive) pour AngularJS basé sur une liste Bootstrap "nav". J'ai ajouté une indentation, des icônes et une animation supplémentaires. Les attributs HTML sont utilisés pour la configuration.

Il n'utilise pas de récursivité.

Je l'ai appelé angular-bootstrap-nav-tree (nom accrocheur, tu ne crois pas?)

Il y a un exemple ici , et la source est ici .

Nick Perkins
la source
1
C'est beau, mais sachez que cela ne fonctionne pas sur la branche Angular 1.0.x.
Danita
3
Oui, il utilise les nouveaux trucs d'animation ... nécessite Angular 1.1.5 (je pense?)
Nick Perkins
3
MISE À JOUR: il fonctionne maintenant avec Angular 1.1.5 ou Angular 1.2.0, et fonctionne également avec Bootsrap 2 ou Bootstrap 3
Nick Perkins
1
Pour info seulement, si vous utilisez Bower, Nick a maintenant rendu cela disponible pour une installation facile - "recherche de bower angular-bootstrap-nav-tree" et "bower install angular-bootstrap-nav-tree --save" et vous avez terminé.
arcseldon
2
@Nick Perkins - pouvez-vous expliquer pourquoi votre arbre angular-bootstrap-nav-tree n'a pas d'API pour supprimer une branche / un nœud. Au moins, à partir d'une inspection rapide de la source et de la vérification de votre test / exemples, il ne semble pas y avoir cette option. C'est sûrement une omission critique?
arcseldon
35

Lorsque vous faites quelque chose comme ça, la meilleure solution est une directive récursive. Cependant, lorsque vous créez une telle directive, vous découvrez qu'AngularJS entre dans une boucle sans fin.

La solution pour cela est de laisser la directive supprimer l'élément pendant l'événement de compilation, puis de les compiler et de les ajouter manuellement dans les événements de lien.

J'ai découvert cela dans ce fil de discussion et j'ai résumé cette fonctionnalité dans un service .

module.factory('RecursionHelper', ['$compile', function($compile){
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param element
         * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function(element, link){
            // Normalize the link parameter
            if(angular.isFunction(link)){
                link = { post: link };
            }

            // Break the recursion loop by removing the contents
            var contents = element.contents().remove();
            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function(scope, element){
                    // Compile the contents
                    if(!compiledContents){
                        compiledContents = $compile(contents);
                    }
                    // Re-add the compiled contents to the element
                    compiledContents(scope, function(clone){
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if(link && link.post){
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);

Avec ce service, vous pouvez facilement créer une directive d'arborescence (ou d'autres directives récursives). Voici un exemple de directive d'arborescence:

module.directive("tree", function(RecursionHelper) {
    return {
        restrict: "E",
        scope: {family: '='},
        template: 
            '<p>{{ family.name }}</p>'+
            '<ul>' + 
                '<li ng-repeat="child in family.children">' + 
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(element) {
            return RecursionHelper.compile(element);
        }
    };
});

Voir ce Plunker pour une démo. J'aime mieux cette solution parce que:

  1. Vous n'avez pas besoin d'une directive spéciale qui rend votre html moins propre.
  2. La logique de récursivité est extraite dans le service RecursionHelper, vous gardez donc vos directives propres.

Mise à jour: Ajout de la prise en charge des fonctions de liaison personnalisées.

Mark Lagendijk
la source
1
cela semble être si net et puissant, une idée pourquoi ce n'est pas un comportement par défaut dans angularjs?
Paul
Lors de l'utilisation de "compiler" comme ceci, comment ajouter des attributs supplémentaires à la portée? La fonction "link" ne semble plus disponible une fois que "compile" est là ...
Brian Kent
1
@ bkent314 J'ai ajouté un support pour cela. Il accepte désormais les fonctions de liaison de la même manière que compile peut les renvoyer. J'ai également créé un projet Github pour le service.
Mark Lagendijk
@MarkLagendijk Très, très élégant! Vous méritez beaucoup de votes positifs pour l'abstraction de la récursion de la directive. Toutes les directives que j'ai vues semblent désespérément compliquées avec cette logique mélangée. Existe-t-il un moyen de faire fonctionner votre RecursionHelper avec la transclusion?
acjay
Je suggère vraiment que vous jetiez des données sur ce type de solution - oui, presque tout le monde implémente un arbre avec des directives récursives, c'est facile. Mais c'est extrêmement lent comme ng-repeat $ digest - une fois que vous avez atteint des centaines de nœuds, cela ne fonctionne pas.
Artemiy
17

angular-ui-tree semble faire du bon travail pour moi

Kalyanaraman Santhanam
la source
15

Voici un exemple utilisant une directive récursive: http://jsfiddle.net/n8dPm/ Tiré de https://groups.google.com/forum/#!topic/angular/vswXTes_FtM

module.directive("tree", function($compile) {
return {
    restrict: "E",
    scope: {family: '='},
    template: 
        '<p>{{ family.name }}</p>'+
        '<ul>' + 
            '<li ng-repeat="child in family.children">' + 
                '<tree family="child"></tree>' +
            '</li>' +
        '</ul>',
    compile: function(tElement, tAttr) {
        var contents = tElement.contents().remove();
        var compiledContents;
        return function(scope, iElement, iAttr) {
            if(!compiledContents) {
                compiledContents = $compile(contents);
            }
            compiledContents(scope, function(clone, scope) {
                     iElement.append(clone); 
            });
        };
    }
};
});
sauvagepanda
la source
J'expérimentais cela, et j'aimerais aussi utiliser la transclusion, pensez-vous que c'est possible?
L.Trabacchin
5

Un autre exemple basé sur la source d'origine , avec un exemple d'arborescence déjà en place (plus facile à voir comment cela fonctionne IMO) et un filtre pour rechercher l'arborescence:

JSFiddle

GFoley83
la source
4

Tant de bonnes solutions, mais je pense qu'elles compliquent toutes un peu les choses d'une manière ou d'une autre.

Je voulais créer quelque chose qui recréait la simplicité du awnser de @Mark Lagendijk, mais sans que cela définisse un modèle dans la directive, mais laisserais plutôt "l'utilisateur" créer le modèle en HTML ...

Avec des idées tirées de https://github.com/stackfull/angular-tree-repeat etc ... j'ai fini par créer le projet: https://github.com/dotJEM/angular-tree

Ce qui vous permet de construire votre arbre comme:

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

Ce qui est pour moi plus propre que de devoir créer plusieurs directives pour des arbres structurés différemment .... En substance, appeler ce qui précède un arbre est un peu faux, cela en tire beaucoup plus de l'arène de "modèles récursifs" de @ ganaraj, mais nous permet de définir le modèle où nous avons besoin de l'arborescence.

(vous pouvez le faire avec un modèle basé sur une balise de script, mais il doit toujours se trouver juste à l'extérieur du nœud d'arbre réel, et cela semble toujours un peu yuk ...)

Laissé ici pour juste un autre choix ...

Jens
la source
MISE À JOUR: À partir de la version 1.5, les directives récursives sont maintenant quelque peu supportées nativement dans Angular. Cela réduit considérablement les cas d'utilisation de dotjem / angular-tree.
Jens
3

Vous pouvez essayer avec l'exemple Angular-Tree-DnD avec Angular-Ui-Tree, mais j'ai édité, compatibilité avec table, grille, liste.

  • Possibilité de glisser-déposer
  • Directive de fonction étendue pour list (next, prev, getChildren, ...)
  • Filtrer les données.
  • OrderBy (ver)
Nguyễn Thiện Hùng
la source
Je vous remercie. J'avais besoin du glisser-déposer, et cela semble être la seule solution avec ça!
Doug
2

Sur la base de l « @ganaraj réponse , et @ dnc253 » de la réponse , Je viens de faire un simple « directive » pour la structure d'arbre ayant la sélection, l' ajout, la suppression et la fonctionnalité d' édition.

Jsfiddle: http://jsfiddle.net/yoshiokatsuneo/9dzsms7y/

HTML:

<script type="text/ng-template" id="tree_item_renderer.html">
    <div class="node"  ng-class="{selected: data.selected}" ng-click="select(data)">
        <span ng-click="data.hide=!data.hide" style="display:inline-block; width:10px;">
            <span ng-show="data.hide && data.nodes.length > 0" class="fa fa-caret-right">+</span>
            <span ng-show="!data.hide && data.nodes.length > 0" class="fa fa-caret-down">-</span>
        </span>
        <span ng-show="!data.editting" ng-dblclick="edit($event)" >{{data.name}}</span>
        <span ng-show="data.editting"><input ng-model="data.name" ng-blur="unedit()" ng-focus="f()"></input></span>
        <button ng-click="add(data)">Add node</button>
        <button ng-click="delete(data)" ng-show="data.parent">Delete node</button>
    </div>
    <ul ng-show="!data.hide" style="list-style-type: none; padding-left: 15px">
        <li ng-repeat="data in data.nodes">
            <recursive><sub-tree data="data"></sub-tree></recursive>
        </li>
    </ul>
</script>
<ul ng-app="Application" style="list-style-type: none; padding-left: 0">
    <tree data='{name: "Node", nodes: [],show:true}'></tree>
</ul>

JavaScript:

angular.module("myApp",[]);

/* https://stackoverflow.com/a/14657310/1309218 */
angular.module("myApp").
directive("recursive", function($compile) {
    return {
        restrict: "EACM",
        require: '^tree',
        priority: 100000,

        compile: function(tElement, tAttr) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents);
                }
                compiledContents(scope, 
                                     function(clone) {
                         iElement.append(clone);
                                         });
            };
        }
    };
});

angular.module("myApp").
directive("subTree", function($timeout) {
    return {
        restrict: 'EA',
        require: '^tree',
        templateUrl: 'tree_item_renderer.html',
        scope: {
            data: '=',
        },
        link: function(scope, element, attrs, treeCtrl) {
            scope.select = function(){
                treeCtrl.select(scope.data);
            };
            scope.delete = function() {
                scope.data.parent.nodes.splice(scope.data.parent.nodes.indexOf(scope.data), 1);
            };
            scope.add = function() {
                var post = scope.data.nodes.length + 1;
                var newName = scope.data.name + '-' + post;
                scope.data.nodes.push({name: newName,nodes: [],show:true, parent: scope.data});
            };
            scope.edit = function(event){
                scope.data.editting = true;
                $timeout(function(){event.target.parentNode.querySelector('input').focus();});
            };
            scope.unedit = function(){
                scope.data.editting = false;
            };

        }
    };
});


angular.module("myApp").
directive("tree", function(){
    return {
        restrict: 'EA',
        template: '<sub-tree data="data" root="data"></sub-tree>',
        controller: function($scope){
            this.select = function(data){
                if($scope.selected){
                    $scope.selected.selected = false;
                }
                data.selected = true;
                $scope.selected = data;
            };
        },
        scope: {
            data: '=',
        }
    }
});
Tsuneo Yoshioka
la source
0

Oui c'est certainement possible. La question ici suppose probablement Angular 1.x, mais pour référence future, j'inclus un exemple Angular 2:

Conceptuellement, tout ce que vous avez à faire est de créer un modèle récursif:

<ul>
    <li *for="#dir of directories">

        <span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()"    /></span> 
        <span (click)="dir.toggle()">{{ dir.name }}</span>

        <div *if="dir.expanded">
            <ul *for="#file of dir.files">
                {{file}}
            </ul>
            <tree-view [directories]="dir.directories"></tree-view>
        </div>
    </li>
</ul>

Vous liez ensuite un objet d'arbre au modèle et laissez Angular opérer sa magie. Ce concept est évidemment également applicable à Angular 1.x.

Voici un exemple complet: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
la source
0

Vous pouvez utiliser angular-recursion-injector pour cela: https://github.com/knyga/angular-recursion-injector

Vous permet de faire une imbrication de profondeur illimitée avec conditionnement. Ne recompilation que si nécessaire et ne compile que les bons éléments. Pas de magie dans le code.

<div class="node">
  <span>{{name}}</span>

  <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

L'une des choses qui lui permet de fonctionner plus rapidement et plus simplement que les autres solutions est le suffixe "--recursion".

Oleksandr Knyga
la source
0

Lorsque l'arborescence est volumineuse, Angular (jusqu'à 1.4.x) devient très lent à rendre un modèle récursif. Après avoir essayé un certain nombre de ces suggestions, j'ai fini par créer une simple chaîne HTML et l'utiliser ng-bind-htmlpour l'afficher. Bien sûr, ce n'est pas la façon d'utiliser les fonctionnalités angulaires

Une fonction récursive simple est présentée ici (avec un minimum de HTML):

function menu_tree(menu, prefix) {
    var html = '<div>' + prefix + menu.menu_name + ' - ' + menu.menu_desc + '</div>\n';
    if (!menu.items) return html;
    prefix += menu.menu_name + '/';
    for (var i=0; i<menu.items.length; ++i) {
        var item = menu.items[i];
        html += menu_tree(item, prefix);
    }
    return html;
}
// Generate the tree view and tell Angular to trust this HTML
$scope.html_menu = $sce.trustAsHtml(menu_tree(menu, ''));

Dans le modèle, il suffit de cette ligne:

<div ng-bind-html="html_menu"></div>

Cela contourne toutes les liaisons de données d'Angular et affiche simplement le HTML en une fraction du temps des méthodes de modèle récursives.

Avec une structure de menu comme celle-ci (une arborescence de fichiers partielle d'un système de fichiers Linux):

menu = {menu_name: '', menu_desc: 'root', items: [
            {menu_name: 'bin', menu_desc: 'Essential command binaries', items: [
                {menu_name: 'arch', menu_desc: 'print machine architecture'},
                {menu_name: 'bash', menu_desc: 'GNU Bourne-Again SHell'},
                {menu_name: 'cat', menu_desc: 'concatenate and print files'},
                {menu_name: 'date', menu_desc: 'display or set date and time'},
                {menu_name: '...', menu_desc: 'other files'}
            ]},
            {menu_name: 'boot', menu_desc: 'Static files of the boot loader'},
            {menu_name: 'dev', menu_desc: 'Device files'},
            {menu_name: 'etc', menu_desc: 'Host-specific system configuration'},
            {menu_name: 'lib', menu_desc: 'Essential shared libraries and kernel modules'},
            {menu_name: 'media', menu_desc: 'Mount point for removable media'},
            {menu_name: 'mnt', menu_desc: 'Mount point for mounting a filesystem temporarily'},
            {menu_name: 'opt', menu_desc: 'Add-on application software packages'},
            {menu_name: 'sbin', menu_desc: 'Essential system binaries'},
            {menu_name: 'srv', menu_desc: 'Data for services provided by this system'},
            {menu_name: 'tmp', menu_desc: 'Temporary files'},
            {menu_name: 'usr', menu_desc: 'Secondary hierarchy', items: [
                {menu_name: 'bin', menu_desc: 'user utilities and applications'},
                {menu_name: 'include', menu_desc: ''},
                {menu_name: 'local', menu_desc: '', items: [
                    {menu_name: 'bin', menu_desc: 'local user binaries'},
                    {menu_name: 'games', menu_desc: 'local user games'}
                ]},
                {menu_name: 'sbin', menu_desc: ''},
                {menu_name: 'share', menu_desc: ''},
                {menu_name: '...', menu_desc: 'other files'}
            ]},
            {menu_name: 'var', menu_desc: 'Variable data'}
        ]
       }

La sortie devient:

- root
/bin - Essential command binaries
/bin/arch - print machine architecture
/bin/bash - GNU Bourne-Again SHell
/bin/cat - concatenate and print files
/bin/date - display or set date and time
/bin/... - other files
/boot - Static files of the boot loader
/dev - Device files
/etc - Host-specific system configuration
/lib - Essential shared libraries and kernel modules
/media - Mount point for removable media
/mnt - Mount point for mounting a filesystem temporarily
/opt - Add-on application software packages
/sbin - Essential system binaries
/srv - Data for services provided by this system
/tmp - Temporary files
/usr - Secondary hierarchy
/usr/bin - user utilities and applications
/usr/include -
/usr/local -
/usr/local/bin - local user binaries
/usr/local/games - local user games
/usr/sbin -
/usr/share -
/usr/... - other files
/var - Variable data
Brent Washburne
la source
-3

Pas compliqué.

<div ng-app="Application" ng-controller="TreeController">
    <table>
        <thead>
            <tr>
                <th>col 1</th>
                <th>col 2</th>
                <th>col 3</th>
            </tr>
        </thead>
        <tbody ng-repeat="item in tree">
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.fname}}</td>
                <td>{{item.lname}}</td>
            </tr>
            <tr ng-repeat="children in item.child">
                <td style="padding-left:15px;">{{children.id}}</td>
                <td>{{children.fname}}</td>
            </tr>
        </tbody>
     </table>
</div>

code contrôleur:

angular.module("myApp", []).
controller("TreeController", ['$scope', function ($scope) {
    $scope.tree = [{
        id: 1,
        fname: "tree",
        child: [{
            id: 1,
            fname: "example"
        }],
        lname: "grid"
    }];


}]);
MBK
la source