¿Es posible hacer una vista de árbol con angular?

177

Estoy buscando mostrar datos en una estructura de árbol en una aplicación web. Esperaba usar Angular para esta tarea.

Parece que ng-repeat me permitirá recorrer una lista de nodos, pero ¿cómo puedo anidar cuando aumenta la profundidad de un nodo dado?

Intenté el siguiente código , pero el escape automático del HTML impide que esto funcione. Además, la etiqueta ul final está en el lugar equivocado.

Estoy bastante seguro de que me estoy ocupando de este problema completamente por el camino equivocado.

¿Algunas ideas?

Jon Abrams
fuente
Acabo de responder esto de una manera bastante genérica sobre una pregunta diferente: stackoverflow.com/questions/14430655/…
tilgovi

Respuestas:

231

Echa un vistazo a este violín

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

Actualizado: http://jsfiddle.net/animaxf/uXbn6/4779/

Esto debería darle una buena idea de cómo mostrar un tree like structureuso angular. Es como usar la recursividad en html

ganaraj
fuente
94
¿Por qué no declaras tu fuente ? escribiste una publicación en ese hilo y ahora estás publicando una url aquí con tu propio nombre?
Janus Troelsen
55
Aquí hay una versión idéntica (creo), excepto que se carga mucho más rápido (al menos para mí), ya que no tiene Twitter Bootstrap incluido en la sección CSS. jsfiddle.net/brendanowen/uXbn6/8
KajMagnus
10
amigo, debes indicar tu fuente.
Ajax3.14
46
Estaba realmente cansado de que la gente constantemente comentara sobre esto que la URL tiene mi nombre (¡y por lo tanto es plagio!). Desafortunadamente, así es como funciona jsfiddle. Si bifurca algo mientras está conectado, conserva su nombre de usuario. Dicho esto, ahora he vinculado a la URL original. Rechace una respuesta si es incorrecta: la respuesta es correcta en este escenario con la única cosa que la URL de respaldo que tenía parece contener mi nombre.
ganaraj
55
Acabo de agregar el botón de colapso y expansión a su versión: jsfiddle.net/uXbn6/639
jbaylina
77

Si está utilizando Bootstrap CSS ...

He creado un control de árbol reutilizable simple (directiva) para AngularJS basado en una lista de "navegación" de Bootstrap. Agregué sangría adicional, íconos y animación. Los atributos HTML se utilizan para la configuración.

No usa recursividad.

Lo llamé angular-bootstrap-nav-tree (nombre atractivo, ¿no te parece?)

Hay un ejemplo aquí , y la fuente está aquí .

Nick Perkins
fuente
1
Es hermoso, pero tenga en cuenta que no funciona en la rama Angular 1.0.x.
Danita
3
Sí, usa las nuevas cosas de animación ... requiere Angular 1.1.5 (¿creo?)
Nick Perkins
3
ACTUALIZACIÓN: ahora funciona con Angular 1.1.5 o Angular 1.2.0, y también funciona con Bootsrap 2 o Bootstrap 3
Nick Perkins el
1
Solo para su información, si usa Bower, Nick ahora lo ha hecho disponible para una fácil instalación: "bower search angular-bootstrap-nav-tree" y "bower install angular-bootstrap-nav-tree --save" y ya está.
arcseldon
2
@Nick Perkins: por favor, ¿puede explicar por qué su angular-bootstrap-nav-tree no tiene API para eliminar una rama / nodo? Al menos, a partir de una inspección rápida de la fuente, y la comprobación de su prueba / ejemplos, no parece ser esa opción. Esta es una omisión crítica, ¿verdad?
arcseldon
35

Al hacer algo como esto, la mejor solución es una directiva recursiva. Sin embargo, cuando haces una directiva de este tipo, descubres que AngularJS entra en un bucle sin fin.

La solución para esto es dejar que la directiva elimine el elemento durante el evento de compilación y compile manualmente y agréguelos en los eventos de enlace.

Descubrí esto en este hilo y abstraje esta funcionalidad en un servicio .

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

Con este servicio, puede crear fácilmente una directiva de árbol (u otras directivas recursivas). Aquí hay un ejemplo de una directiva de árbol:

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

Vea este Plunker para una demostración. Esta solución me gusta más porque:

  1. No necesita una directiva especial que haga que su html sea menos limpio.
  2. La lógica de recursión se abstrae en el servicio RecursionHelper, por lo que mantiene sus directivas limpias.

Actualización: Se agregó soporte para funciones de enlace personalizadas.

Mark Lagendijk
fuente
1
esto parece ser tan ordenado y poderoso, ¿alguna idea de por qué este no es un comportamiento predeterminado en angularjs?
Paul
Cuando se usa "compilar" de esta manera, ¿cómo se agregan atributos adicionales al alcance? La función "enlace" ya no está disponible una vez que "compilar" está allí ...
Brian Kent
1
@ bkent314 Agregué soporte para esto. Ahora acepta funciones de enlace de la misma manera que la compilación puede devolverlas. También creé un proyecto Github para el servicio.
Mark Lagendijk
@MarkLagendijk ¡Muy, muy hábil! Usted merece muchos votos a favor por abstraer la recursividad de la directiva. Todas las directivas que he visto parecen irremediablemente complicadas con esa lógica mezclada. ¿Hay alguna manera de hacer que su RecursionHelper funcione con transclusión?
acjay
Realmente sugiero que arroje algunos datos a este tipo de solución: sí, casi todo el mundo implementa el árbol con directivas recursivas, es fácil. Pero es extremadamente lento como ng-repeat $ digest's: una vez que llega a cientos de nodos, esto no funciona.
Artemiy
15

Aquí hay un ejemplo usando una directiva recursiva: http://jsfiddle.net/n8dPm/ Tomado 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); 
            });
        };
    }
};
});
savagepanda
fuente
Estaba experimentando con esto, y me gustaría usar la transclusión también, ¿crees que es posible?
L.Trabacchin
5

Otro ejemplo basado en la fuente original , con una estructura de árbol de muestra ya establecida (es más fácil ver cómo funciona IMO) y un filtro para buscar en el árbol:

JSFiddle

GFoley83
fuente
4

Tantas soluciones geniales, pero creo que todas de una forma u otra complican un poco las cosas.

Quería crear algo que recreara la simplicidad del abanico de @Mark Lagendijk, pero sin definir una plantilla en la directiva, sino que permitiría que el "usuario" creara la plantilla en HTML ...

Con ideas tomadas de https://github.com/stackfull/angular-tree-repeat, etc., terminé creando el proyecto: https://github.com/dotJEM/angular-tree

Lo que te permite construir tu árbol como:

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

Lo que para mí es más limpio que tener que crear múltiples directivas para árboles estructurados de manera diferente ... En esencia, llamar a lo anterior un árbol es un poco falso, saca mucho más de la "plantilla recursiva" de @ ganaraj, pero nos permite Definir la plantilla donde necesitamos el árbol.

(podría hacerlo con una plantilla basada en la etiqueta del script, pero todavía tiene que estar justo fuera del nodo del árbol real, y todavía se siente un poco asqueroso ...)

Dejado aquí para otra opción ...

Jens
fuente
ACTUALIZACIÓN: A partir de 1.5 las directivas recursivas ahora son algo nativamente compatibles en Angular. Esto reduce mucho los casos de uso de dotjem / angular-tree.
Jens
3

Puede probar con Angular-Tree-DnD sample con Angular-Ui-Tree, pero edité , compatibilidad con tabla, cuadrícula, lista.

  • Capaz de arrastrar y soltar
  • Directiva de función extendida para la lista (siguiente, anterior, getChildren, ...)
  • Filtrar datos.
  • OrderBy (ver)
Nguyễn Thiện Hùng
fuente
Gracias. Necesitaba arrastrar y soltar, ¡y esta parece ser la única solución con eso!
Doug
2

Sobre la base de @ganaraj 's respuesta , y dnc253 @' s respuesta , acabo de hacer un simple 'directiva' de la estructura de árbol que tiene seleccionar, añadir, eliminar y editar función.

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

Sí, definitivamente es posible. La pregunta aquí probablemente asume Angular 1.x, pero para referencia futura incluyo un ejemplo de Angular 2:

Conceptualmente, todo lo que tiene que hacer es crear una plantilla recursiva:

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

Luego vincula un objeto de árbol a la plantilla y deja que Angular haga su magia. Este concepto es obviamente aplicable a Angular 1.x también.

Aquí hay un ejemplo completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
fuente
0

Puede usar el inyector de recursión angular para eso: https://github.com/knyga/angular-recursion-injector

Le permite anidar en profundidad ilimitada con acondicionamiento. Recompila solo si es necesario y solo compila los elementos correctos. No hay magia en el código.

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

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

Una de las cosas que le permite trabajar más rápido y más simple que las otras soluciones es el sufijo "--recursion".

Oleksandr Knyga
fuente
0

Cuando la estructura de árbol es grande, Angular (hasta 1.4.x) se vuelve muy lento al representar una plantilla recursiva. Después de probar varias de estas sugerencias, terminé creando una cadena HTML simple y usándola ng-bind-htmlpara mostrarla. Por supuesto, esta no es la forma de usar las características angulares

Aquí se muestra una función recursiva básica (con un mínimo 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, ''));

En la plantilla, solo necesita esta línea:

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

Esto omite todos los enlaces de datos de Angular y simplemente muestra el HTML en una fracción del tiempo de los métodos de plantilla recursiva.

Con una estructura de menú como esta (un árbol de archivos parcial de un sistema de archivos 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 salida se convierte en:

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

No complicado.

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

código del controlador:

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


}]);
MBK
fuente