Czy można utworzyć widok drzewa za pomocą Angular?

177

Chcę wyświetlić dane w strukturze drzewa w aplikacji internetowej. Miałem nadzieję, że do tego zadania wykorzystam Angular.

Wygląda na to, że ng-repeat pozwoli mi na iterację przez listę węzłów, ale jak mogę wtedy zagnieżdżać, gdy głębokość danego węzła wzrośnie?

Wypróbowałem następujący kod , ale automatyczne uciekanie kodu HTML uniemożliwia to działanie. Ponadto końcowy znacznik ul jest w złym miejscu.

Jestem prawie pewien, że podchodzę do tego problemu całkowicie w niewłaściwy sposób.

Jakieś pomysły?

Jon Abrams
źródło
Właśnie odpowiedziałem na to w dość ogólny sposób na inne pytanie: stackoverflow.com/questions/14430655/ ...
tilgovi

Odpowiedzi:

231

Spójrz na to skrzypce

Oryginał: http://jsfiddle.net/brendanowen/uXbn6/8/

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

To powinno dać ci dobry pomysł, jak wyświetlić tree like structureusing angular. To trochę rekurencja w html!

ganaraj
źródło
94
dlaczego nie podać źródła ? napisałeś post w tym wątku, a teraz umieszczasz tutaj adres URL ze swoim własnym imieniem?
Janus Troelsen
5
Tutaj jest identyczna wersja (tak mi się wydaje), z tym wyjątkiem, że ładuje się znacznie szybciej (przynajmniej dla mnie), ponieważ nie ma wbudowanego Twittera Bootstrap w sekcji CSS. jsfiddle.net/brendanowen/uXbn6/8
KajMagnus
10
koleś, powinieneś podać swoje źródło.
Ajax 3.14
46
Byłem naprawdę zmęczony ludźmi, którzy ciągle komentowali to, że URL ma w sobie moje imię (i dlatego jest to plagiat!). Tak niestety działa jsfiddle. Jeśli coś rozwidlisz, gdy jesteś zalogowany, zachowuje twoją nazwę użytkownika. Powiedziawszy, że mam teraz link do oryginalnego adresu URL. Głosuj negatywnie na odpowiedź, jeśli jest błędna - odpowiedź jest poprawna w tym scenariuszu, z tym, że zapasowy adres URL, który miałem, wydaje się zawierać moje imię i nazwisko.
ganaraj
5
Właśnie dodałem przycisk zwijania i rozwijania do twojej wersji: jsfiddle.net/uXbn6/639
jbaylina
77

Jeśli używasz Bootstrap CSS ...

Stworzyłem prostą kontrolę drzewa (dyrektywę) wielokrotnego użytku dla AngularJS na podstawie listy "nav" Bootstrap. Dodałem dodatkowe wcięcia, ikony i animację. Do konfiguracji używane są atrybuty HTML.

Nie używa rekursji.

Nazwałem to angular-bootstrap-nav-tree (chwytliwa nazwa, nie sądzisz?)

Jest przykładem tutaj , a źródło jest tutaj .

Nick Perkins
źródło
1
Jest piękny, ale ostrzegamy, że nie działa na gałęzi Angular 1.0.x.
Danita
3
Tak, wykorzystuje nowe elementy animacji ... wymaga Angular 1.1.5 (tak myślę?)
Nick Perkins
3
AKTUALIZACJA: działa teraz z Angular 1.1.5 lub Angular 1.2.0, a także działa z Bootsrap 2 lub Bootstrap 3
Nick Perkins
1
Tylko do Twojej wiadomości, jeśli używasz Bower, Nick udostępnił to do łatwej instalacji - "bower search angular-bootstrap-nav-tree" i "bower install angular-bootstrap-nav-tree --save" i gotowe.
arcseldon
2
@Nick Perkins - czy możesz wyjaśnić, dlaczego twoje angular-bootstrap-nav-tree nie ma API do usuwania gałęzi / węzła. Przynajmniej po szybkiej inspekcji źródła i sprawdzeniu testów / przykładów wydaje się, że nie ma takiej opcji. To z pewnością krytyczne zaniedbanie?
arcseldon
35

Przy tworzeniu czegoś takiego najlepszym rozwiązaniem jest dyrektywa rekurencyjna. Jednak kiedy tworzysz taką dyrektywę, okazuje się, że AngularJS wpada w nieskończoną pętlę.

Rozwiązaniem tego problemu jest zezwolenie dyrektywie na usunięcie elementu podczas zdarzenia kompilacji oraz ręczne skompilowanie i dodanie ich do zdarzeń łącza.

Dowiedziałem się o tym w tym wątku i wyekstrahowałem tę funkcję w usłudze .

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

Dzięki tej usłudze możesz łatwo utworzyć dyrektywę drzewa (lub inne dyrektywy rekurencyjne). Oto przykład dyrektywy drzewa:

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

Zobacz ten Plunker, aby zobaczyć demo. Najbardziej podoba mi się to rozwiązanie, ponieważ:

  1. Nie potrzebujesz specjalnej dyrektywy, która sprawia, że ​​Twój HTML jest mniej przejrzysty.
  2. Logika rekurencji jest wyodrębniana do usługi RecursionHelper, dzięki czemu dyrektywy są czyste.

Aktualizacja: Dodano obsługę niestandardowych funkcji łączenia.

Mark Lagendijk
źródło
1
wydaje się to być tak zgrabne i potężne, czy masz pojęcie, dlaczego nie jest to domyślne zachowanie w angularjs?
Paweł
Kiedy używasz „kompiluj” w ten sposób, w jaki sposób dodać dodatkowe atrybuty do zakresu? Wydaje się, że funkcja "link" nie jest już dostępna, gdy pojawi się funkcja "kompilacji" ...
Brian Kent
1
@ bkent314 Dodałem obsługę tego. Teraz akceptuje funkcje łączące w taki sam sposób, w jaki kompilacja może je zwrócić. Stworzyłem również projekt Github dla usługi.
Mark Lagendijk,
@MarkLagendijk Very, very slick! Zasługujesz na wiele głosów za wyjęciem rekursji z dyrektywy. Wszystkie dyrektywy, które widziałem, wyglądają na beznadziejnie skomplikowane z tą logiką zmieszaną. Czy istnieje sposób, aby Twój RecursionHelper działał z transkluzją?
acjay
Naprawdę sugeruję, abyś wrzucił trochę danych do tego typu rozwiązania - tak, prawie każdy implementuje drzewo z dyrektywami rekurencyjnymi, to proste. Ale jest bardzo powolny, jak ng-repeat $ Digest's - gdy dotrzesz do setek węzłów, nie działa.
Artemiy
17

angular-ui-tree wydaje mi się dobrą robotą

Kalyanaraman Santhanam
źródło
15

Oto przykład z użyciem dyrektywy rekurencyjnej: http://jsfiddle.net/n8dPm/ Pobrano z 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
źródło
eksperymentowałem z tym i chciałbym też użyć transkluzji, myślisz, że to możliwe?
L.Trabacchin,
5

Kolejny przykład oparty na oryginalnym źródle , z już wdrożoną strukturą przykładowego drzewa (łatwiejsze do zobaczenia, jak to działa IMO) i filtrem do przeszukiwania drzewa:

JSFiddle

GFoley83
źródło
4

Tyle świetnych rozwiązań, ale wydaje mi się, że wszystkie one w jakiś sposób zbytnio komplikują sprawę.

Chciałem stworzyć coś, co odtworzyłoby prostotę awnsera @Marka Lagendijka, ale bez definiowania szablonu w dyrektywie, ale raczej pozwoliłoby "użytkownikowi" stworzyć szablon w HTML ...

Z pomysłami zaczerpniętymi z https://github.com/stackfull/angular-tree-repeat etc ... Skończyło się na stworzeniu projektu: https://github.com/dotJEM/angular-tree

Co pozwala ci zbudować drzewo, takie jak:

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

Co dla mnie jest czystsze niż konieczność tworzenia wielu dyrektyw dla drzew o różnej strukturze .... W istocie wywołanie powyższego drzewa jest trochę fałszywe, wybiera znacznie więcej z narzędzia @ ganaraj w zakresie „szablonów rekurencyjnych”, ale pozwala nam zdefiniuj szablon, w którym potrzebujemy drzewa.

(możesz to zrobić za pomocą szablonu opartego na tagu skryptu, ale nadal musi on znajdować się tuż poza faktycznym węzłem drzewa i nadal wydaje się trochę fuj ...)

Zostawiłem tutaj kolejny wybór ...

Jens
źródło
AKTUALIZACJA: Od wersji 1.5 dyrektywy rekurencyjne są teraz nieco natywnie obsługiwane w Angular. To bardzo zawęża przypadki użycia dotjem / angular-tree.
Jens
3

Możesz spróbować z próbką Angular-Tree-DnD z Angular-Ui-Tree, ale edytowałem, zgodność z tabelą, siatką, listą.

  • Możliwość przeciągania i upuszczania
  • Rozszerzona dyrektywa funkcji dla list (next, prev, getChildren, ...)
  • Filtruj dane.
  • OrderBy (wer)
Nguyễn Thiện Hùng
źródło
Dziękuję Ci. Potrzebowałem Drag & Drop, a to wydaje się być jedynym rozwiązaniem!
Doug,
2

Na podstawie @ganaraj „s odpowiedź , a @ dnc253” s odpowiedź , po prostu popełnił prosty «Dyrektywa» dla struktury drzewa mające doborem, dodawanie, usuwanie i edycję funkcję.

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
źródło
0

Tak, to zdecydowanie możliwe. Pytanie tutaj prawdopodobnie zakłada Angular 1.x, ale dla przyszłych odniesień dołączam przykład Angular 2:

Koncepcyjnie wszystko, co musisz zrobić, to utworzyć szablon rekurencyjny:

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

Następnie wiążesz obiekt drzewa z szablonem i pozwalasz Angularowi działać jego magia. Ta koncepcja ma oczywiście zastosowanie również do Angular 1.x.

Oto pełny przykład: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
źródło
0

Możesz do tego użyć wtryskiwacza rekurencyjnego kątowego: https://github.com/knyga/angular-recursion-injector

Umożliwia zagnieżdżanie nieograniczonej głębokości z kondycjonowaniem. Dokonuje ponownej kompilacji tylko w razie potrzeby i kompiluje tylko właściwe elementy. Żadnej magii w kodzie.

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

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

Jedną z rzeczy, która pozwala mu działać szybciej i prościej niż inne rozwiązania, jest przyrostek „--recursion”.

Oleksandr Knyga
źródło
0

Gdy struktura drzewa jest duża, Angular (do 1.4.x) bardzo wolno renderuje szablon rekurencyjny. Po wypróbowaniu kilku z tych sugestii utworzyłem prosty ciąg znaków HTML i użyłem ng-bind-htmlgo do wyświetlenia. Oczywiście nie jest to sposób na używanie funkcji Angulara

Poniżej pokazano funkcję rekurencyjną w wersji podstawowej (z minimalnym kodem 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, ''));

W szablonie potrzebuje tylko tej jednej linii:

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

Pomija to całe wiązanie danych Angulara i po prostu wyświetla kod HTML w ułamku czasu rekursywnych metod szablonu.

Z taką strukturą menu (częściowe drzewo plików systemu plików 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'}
        ]
       }

Wynik staje się:

- 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
źródło
-3

Nieskomplikowany.

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

kod kontrolera:

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


}]);
MBK
źródło