È possibile creare una vista ad albero con angolare?


177

Sto cercando di visualizzare i dati in una struttura ad albero in un'app Web. Speravo di usare Angular per questo compito.

Sembra che ng-repeat mi permetta di scorrere un elenco di nodi, ma come posso fare l'annidamento quando aumenta la profondità di un dato nodo?

Ho provato il seguente codice , ma l'escaping automatico dell'HTML impedisce che funzioni. Inoltre, il tag end ul è nel posto sbagliato.

Sono abbastanza sicuro di affrontare questo problema completamente nel modo sbagliato.

Qualche idea?


Ho appena risposto questo in un modo piuttosto generico su su una questione diversa: stackoverflow.com/questions/14430655/...
tilgovi

Risposte:


231

Dai un'occhiata a questo violino

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

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

Questo dovrebbe darti una buona idea di come visualizzare un tree like structureuso angolare. È un po 'come usare la ricorsione in html!


94
perché non dichiarare la tua fonte ? hai scritto un post in quella discussione e ora stai pubblicando un URL qui con il tuo nome?
Janus Troelsen,

5
Ecco una versione identica (credo), tranne per il fatto che si carica molto più velocemente (almeno per me), dal momento che non ha Twitter Bootstrap integrato nella sezione CSS. jsfiddle.net/brendanowen/uXbn6/8
KajMagnus

10
amico dovresti dichiarare la tua fonte.
Ajax 3.14,

46
Ero davvero stanco delle persone che commentavano costantemente che l'URL aveva il mio nome (e quindi è plagio!). Sfortunatamente è così che funziona jsfiddle. Se esegui il fork di qualcosa mentre sei loggato, conserva il tuo nome utente. Detto questo, ora ho collegato l'URL originale. Declassare una risposta se è errata - La risposta sembra essere corretta in questo scenario con l'unica cosa che l'URL di backup che avevo sembra contenere il mio nome in esso.
Ganaraj,

5
Ho appena aggiunto il pulsante comprimi ed espandi alla tua versione: jsfiddle.net/uXbn6/639
jbaylina,

77

Se si utilizza Bootstrap CSS ...

Ho creato un semplice controllo ad albero riutilizzabile (direttiva) per AngularJS basato su un elenco "nav" di Bootstrap. Ho aggiunto rientri, icone e animazioni extra. Gli attributi HTML vengono utilizzati per la configurazione.

Non utilizza la ricorsione.

L'ho chiamato angular-bootstrap-nav-tree (nome accattivante, non credi?)

C'è un esempio qui e la fonte è qui .


1
È bello, ma tieni presente che non funziona sul ramo Angular 1.0.x.
Danita,

3
Sì, utilizza la nuova roba dell'animazione ... richiede Angular 1.1.5 (penso?)
Nick Perkins,

3
AGGIORNAMENTO: ora funziona con Angular 1.1.5 o Angular 1.2.0 e funziona anche con Bootsrap 2 o Bootstrap 3
Nick Perkins,

1
Cordiali saluti, se si utilizza Bower, Nick ora lo ha reso disponibile per una facile installazione - "bower search angular-bootstrap-nav-tree" e "bower install angular-bootstrap-nav-tree --save" e il gioco è fatto.
arcseldon,

2
@ Nick Perkins - per favore, puoi spiegare perché il tuo angular-bootstrap-nav-tree non ha API per rimuovere un Branch / Nodo. Almeno, da una rapida ispezione della fonte e dal controllo del tuo test / esempi non sembra esserci questa opzione. Questa è un'omissione critica, sicuramente?
arcseldon,

35

Quando si fa qualcosa del genere, la soluzione migliore è una direttiva ricorsiva. Tuttavia, quando fai una direttiva del genere scopri che AngularJS entra in un ciclo infinito.

La soluzione per questo è lasciare che la direttiva rimuova l'elemento durante l'evento di compilazione e compili e li aggiunge manualmente negli eventi di collegamento.

Ho scoperto questo in questo thread e ho estratto questa funzionalità in un servizio .

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 questo servizio puoi facilmente creare una direttiva sugli alberi (o altre direttive ricorsive). Ecco un esempio di una direttiva ad albero:

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

Guarda questo Plunker per una demo. Mi piace di più questa soluzione perché:

  1. Non hai bisogno di una direttiva speciale che renda il tuo HTML meno pulito.
  2. La logica di ricorsione viene sottratta al servizio RecursionHelper, in modo da mantenere pulite le direttive.

Aggiornamento: aggiunto il supporto per funzioni di collegamento personalizzate.


1
questo sembra essere così pulito e potente, hai idea del perché questo non è un comportamento predefinito in angularjs?
Paul,

Quando si utilizza "compile" in questo modo, come si aggiungono ulteriori attributi all'ambito? La funzione "link" non sembra più disponibile una volta che "compile" è lì ...
Brian Kent,

1
@ bkent314 Ho aggiunto il supporto per questo. Ora accetta le funzioni di collegamento allo stesso modo in cui la compilazione può restituirle. Ho anche creato un progetto Github per il servizio.
Mark Lagendijk,

@MarkLagendijk Molto, molto lucido! Meriti molti voti positivi per aver sottratto la ricorsione alla direttiva. Tutte le direttive che ho visto sembrano irrimediabilmente complicate da quella logica confusa. Esiste un modo per far funzionare il tuo RecursionHelper con la inclusione?
Acjay

Consiglio davvero di lanciare alcuni dati su questo tipo di soluzione: sì, quasi tutti implementano l'albero con direttive ricorsive, è facile. Ma è estremamente lento come ng-repeat $ digest - una volta arrivati ​​a centinaia di nodi, questo non funziona.
Artemiy,


15

Ecco un esempio usando una direttiva ricorsiva: http://jsfiddle.net/n8dPm/ Tratto da 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); 
            });
        };
    }
};
});

lo stavo sperimentando e mi piacerebbe anche usare la trasclusione, pensi sia possibile?
L.Trabacchin,


5

Un altro esempio basato sulla fonte originale , con una struttura ad albero di esempio già in atto (più facile da vedere come funziona IMO) e un filtro per cercare l'albero:

JSFiddle


4

Tante fantastiche soluzioni, ma le sento tutte in un modo o nell'altro complicare un po 'le cose.

Volevo creare qualcosa che ricreasse la semplicità del tendalino di @Mark Lagendijk, ma senza che questo definisse un modello nella direttiva, ma piuttosto avrei lasciato che "l'utente" creasse il modello in HTML ...

Con idee tratte da https://github.com/stackfull/angular-tree-repeat ecc ... ho finito per creare il progetto: https://github.com/dotJEM/angular-tree

Ciò ti consente di costruire il tuo albero come:

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

Il che per me è più pulito che dover creare più direttive per alberi strutturati in modo diverso .... In sostanza, chiamare un albero sopra è un po 'falso, prende molto di più dalla tenda dei "modelli ricorsivi" di @ ganaraj, ma ci permette di definire il modello in cui è necessario l'albero.

(potresti farlo con un modello basato su tag script, ma deve comunque trovarsi proprio fuori dal nodo dell'albero reale e sembra ancora un po 'yuk ...)

Lasciato qui solo per un'altra scelta ...


AGGIORNAMENTO: A partire da 1.5 direttive ricorsive sono ora in qualche modo supportate in modo nativo in Angular. Ciò riduce notevolmente i casi d'uso di dotjem / angular-tree.
Jens,

3

Puoi provare con il campione Angular-Tree-DnD con Angular-Ui-Tree, ma ho modificato, compatibilità con tabella, griglia, elenco.

  • In grado di trascinare e rilasciare
  • Direttiva sulle funzioni estese per elenco (next, prev, getChildren, ...)
  • Filtra i dati.
  • OrderBy (ver)

Grazie. Avevo bisogno del Drag & Drop, e questa sembra essere l'unica soluzione!
Doug

2

Sulla base di @ganaraj 's risposta , e @ dnc253' s risposta , ho appena fatto un semplice 'direttiva' per la struttura ad albero con la selezione, l'aggiunta, l'eliminazione e la modifica di funzionalità.

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

0

Sì, sicuramente possibile. La domanda qui presume probabilmente Angular 1.x, ma per riferimento futuro includo un esempio Angular 2:

Concettualmente, tutto ciò che devi fare è creare un modello ricorsivo:

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

Quindi associ un oggetto albero al modello e lasci che Angular funzioni la sua magia. Questo concetto è ovviamente applicabile anche ad Angular 1.x.

Ecco un esempio completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


0

A tale scopo è possibile utilizzare l'iniettore di ricorsione angolare: https://github.com/knyga/angular-recursion-injector

Consente di eseguire annidamenti di profondità illimitati con condizionamento. Compila solo se necessario e compila solo gli elementi giusti. Nessuna magia nel codice.

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

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

Una delle cose che gli permette di lavorare più velocemente e più facilmente delle altre soluzioni è il suffisso "--recursion".


0

Quando la struttura ad albero è grande, Angolare (fino a 1.4.x) diventa molto lento nel rendering di un modello ricorsivo. Dopo aver provato alcuni di questi suggerimenti, ho finito per creare una semplice stringa HTML e usarla ng-bind-htmlper visualizzarla. Naturalmente, questo non è il modo di usare le funzioni angolari

Una funzione ricorsiva bare-bones è mostrata qui (con HTML minimo):

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

Nel modello, necessita solo di questa riga:

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

Ciò ignora tutti i dati di Angular e visualizza semplicemente l'HTML in una frazione del tempo dei metodi del modello ricorsivo.

Con una struttura di menu come questa (un albero di file parziale di un file system 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'}
        ]
       }

L'output diventa:

- 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

-3

Non complicato.

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

codice controller:

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


}]);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.