É possível fazer uma exibição em árvore com angular?

177

Estou procurando exibir dados em uma estrutura de árvore em um aplicativo da web. Eu esperava usar o Angular para esta tarefa.

Parece que ng-repeat me permitirá percorrer uma lista de nós, mas como posso aninhar quando a profundidade de um determinado nó aumenta?

Eu tentei o código a seguir , mas o escape automático do HTML está impedindo que isso funcione. Além disso, a tag final ul está no lugar errado.

Tenho certeza de que estou lidando com esse problema de maneira totalmente errada.

Alguma ideia?

Jon Abrams
fonte
Eu apenas respondi isso de uma maneira bastante genérica sobre sobre uma questão diferente: stackoverflow.com/questions/14430655/...
tilgovi

Respostas:

231

Dê uma olhada neste violino

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

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

Isso deve lhe dar uma boa idéia de como exibir um tree like structureangular de uso. É como usar recursão em html!

ganaraj
fonte
94
por que não indicar sua fonte ? você escreveu um post nesse tópico e agora está postando um URL aqui com seu próprio nome?
Janus Troelsen
5
Aqui está uma versão idêntica (eu acho), exceto que ela carrega muito mais rápido (pelo menos para mim), já que não possui o Twitter Bootstrap embutido na seção CSS. jsfiddle.net/brendanowen/uXbn6/8
KajMagnus
10
cara, você deve indicar sua fonte.
Ajax3.14
46
Eu estava realmente cansado de as pessoas comentarem constantemente que a URL tem meu nome (e, portanto, é plágio!). Infelizmente, é assim que o jsfiddle funciona. Se você bifurcar algo enquanto estiver logado, ele manterá seu nome de usuário. Dito isto, agora criei um link para o URL original. Diminuir a votação de uma resposta se estiver errada - a resposta está correta nesse cenário, com a única coisa que o URL de backup que eu tinha parece conter o meu nome.
precisa saber é
5
Acabei de adicionar o colapso e expandir botão para a sua versão: jsfiddle.net/uXbn6/639
jbaylina
77

Se você estiver usando o Bootstrap CSS ...

Eu criei um controle de árvore reutilizável simples (diretiva) para o AngularJS com base em uma lista "nav" do Bootstrap. Eu adicionei recuo extra, ícones e animação. Atributos HTML são usados ​​para configuração.

Não usa recursão.

Eu chamei de angular-bootstrap-nav-tree (nome cativante, você não acha?)

Há um exemplo aqui e a fonte está aqui .

Nick Perkins
fonte
1
É bonito, mas esteja avisado de que não funciona no ramo Angular 1.0.x.
Danita
3
Sim, ele usa o novo material de animação ... requer Angular 1.1.5 (eu acho?)
Nick Perkins
3
UPDATE: agora funciona com qualquer Angular 1.1.5 ou 1.2.0 angular, e também funciona com qualquer Bootsrap 2 ou Bootstrap 3
Nick Perkins
1
Apenas para sua informação, se você estiver usando o Bower, Nick agora disponibilizou isso para facilitar a instalação - "busca do bower angular-bootstrap-nav-tree" e "bower install angular - bootstrap-nav-tree --save" e pronto.
Arcseldon
2
@Nick Perkins - você pode explicar por que sua angular-bootstrap-nav-tree não possui API para remover uma filial / nó. Pelo menos, a partir de uma inspeção rápida da fonte e a verificação de seus testes / exemplos, parece não haver essa opção. Esta é uma omissão crítica, com certeza?
Arcseldon 11/05/2014
35

Ao fazer algo assim, a melhor solução é uma diretiva recursiva. No entanto, quando você faz essa diretiva, descobre que o AngularJS entra em um loop infinito.

A solução para isso é permitir que a diretiva remova o elemento durante o evento de compilação, compile e adicione manualmente nos eventos de link.

Eu descobri sobre isso neste tópico e abstraí essa funcionalidade em um serviço .

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

Com este serviço, você pode facilmente criar uma diretiva de árvore (ou outras diretivas recursivas). Aqui está um exemplo de uma diretiva de árvore:

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

Veja este Plunker para uma demonstração. Eu gosto mais desta solução porque:

  1. Você não precisa de uma diretiva especial que torne seu html menos limpo.
  2. A lógica de recursão é abstraída para o serviço RecursionHelper, para que você mantenha suas diretivas limpas.

Atualização: Adicionado suporte para funções de vinculação personalizadas.

Mark Lagendijk
fonte
1
isso parece ser tão elegante e poderoso, alguma idéia de por que esse não é um comportamento padrão no angularjs?
Paul
Ao usar "compilar" assim, como adicionar atributos adicionais ao escopo? A função "link" parece já não está disponível uma vez "compilação" está lá ...
Brian Kent
1
@ bkent314 Adicionei suporte para isso. Agora ele aceita funções de vinculação da mesma maneira que a compilação pode devolvê-las. Também criei um projeto Github para o serviço.
Mark Lagendijk
@MarkLagendijk Very, very slick! Você merece muitos votos positivos por abstrair a recursão da diretiva. Todas as diretivas que vi parecem irremediavelmente complicadas com essa lógica misturada. Existe uma maneira de fazer seu RecursionHelper funcionar com a transclusão?
acjay
Eu realmente sugiro que você jogue alguns dados nesse tipo de solução - sim, quase todo mundo implementa árvore com diretivas recursivas, é fácil. Mas é extremamente lento como dig-ng $ digest - quando você chega a centenas de nós, isso não funciona.
Artemiy #
17

angular-ui-tree parece fazer um bom trabalho para mim

Kalyanaraman Santhanam
fonte
15

Aqui está um exemplo usando uma diretiva recursiva: http://jsfiddle.net/n8dPm/ Retirado 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
fonte
estava experimentando isso e gostaria de usar a transclusão também, você acha que é possível?
L.Trabacchin
5

Outro exemplo baseado na fonte original , com uma estrutura de árvore de amostra já em vigor (mais fácil de ver como funciona o IMO) e um filtro para pesquisar na árvore:

JSFiddle

GFoley83
fonte
4

Tantas ótimas soluções, mas acho que todas elas, de uma forma ou de outra, complicam um pouco as coisas.

Eu queria criar algo que recriasse a simplicidade do awnser de @Mark Lagendijk, mas sem definir um modelo na diretiva, mas permitiria que o "usuário" criasse o modelo em HTML ...

Com idéias tiradas de https://github.com/stackfull/angular-tree-repeat etc ... acabei criando o projeto: https://github.com/dotJEM/angular-tree

O que permite que você construa sua árvore como:

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

O que, para mim, é mais limpo do que ter que criar várias diretivas para árvores de estrutura diferente ... Em essência, chamar a árvore acima é um pouco falsa, ela tira muito mais proveito do awnser de "modelos recursivos" do @ ganaraj, mas nos permite defina o modelo em que precisamos da árvore.

(você pode fazer isso com um modelo baseado em tag de script, mas ele ainda precisa ficar do lado de fora do nó da árvore real e ainda parece um pouco estranho ...)

Deixou aqui apenas mais uma opção ...

Jens
fonte
ATUALIZAÇÃO: A partir de 1,5, as diretivas recursivas agora são suportadas de forma nativa no Angular. Isso reduz bastante os casos de uso de dotjem / angular-tree.
Jens
3

Você pode tentar com o exemplo Angular-Tree-DnD com Angular-Ui-Tree, mas editei, compatibilidade com tabela, grade, lista.

  • Capaz de arrastar e soltar
  • Diretiva de função estendida para lista (next, prev, getChildren, ...)
  • Filtrar dados.
  • OrderBy (ver)
Nguyễn Thiện Hùng
fonte
Obrigado. Eu precisava do Drag & Drop, e essa parece ser a única solução!
Doug
2

Baseado em @ganaraj 's resposta , e @ dnc253' s resposta , eu fiz apenas um simples 'directiva' para a estrutura da árvore ter selecionar, adicionar, excluir e editar recurso.

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

Sim, definitivamente possível. A pergunta aqui provavelmente assume o Angular 1.x, mas para referência futura, incluí um exemplo do Angular 2:

Conceitualmente, tudo o que você precisa fazer é criar um modelo recursivo:

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

Em seguida, você vincula um objeto de árvore ao modelo e deixa o Angular trabalhar sua mágica. Obviamente, esse conceito também é aplicável ao Angular 1.x.

Aqui está um exemplo completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
fonte
0

Você pode usar o injetor de recursão angular para isso: https://github.com/knyga/angular-recursion-injector

Permite fazer aninhamentos de profundidade ilimitados com condicionamento. Recompila apenas se necessário e compila apenas os elementos corretos. Nenhuma mágica no código.

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

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

Uma das coisas que permite que ele trabalhe mais rápido e mais simples que as outras soluções é o sufixo "--recursion".

Oleksandr Knyga
fonte
0

Quando a estrutura da árvore é grande, o Angular (até 1.4.x) fica muito lento ao renderizar um modelo recursivo. Depois de tentar várias dessas sugestões, acabei criando uma string HTML simples e usando-a ng-bind-htmlpara exibi-la. Obviamente, esse não é o modo de usar os recursos angulares

Uma função recursiva básica é mostrada aqui (com HTML mínimo):

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, ''));

No modelo, ele precisa apenas dessa linha:

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

Isso ignora toda a ligação de dados do Angular e simplesmente exibe o HTML em uma fração do tempo dos métodos de modelo recursivo.

Com uma estrutura de menus como esta (uma árvore de arquivos parcial de um sistema de arquivos 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'}
        ]
       }

A saída se torna:

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

Não é 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 do controlador:

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


}]);
MBK
fonte