Angular로 Tree View를 만들 수 있습니까?


177

웹 앱에서 데이터를 트리 구조로 표시하려고합니다. 이 작업에 Angular를 사용하고 싶었습니다.

ng-repeat를 사용하면 노드 목록을 반복 할 수 있지만 주어진 노드의 깊이가 증가하면 어떻게 중첩 할 수 있습니까?

다음 코드를 시도했지만 HTML의 자동 이스케이프로 인해 작동하지 않습니다. 또한 최종 ul 태그가 잘못된 위치에 있습니다.

나는이 문제에 대해 완전히 잘못된 길로 가고 있다고 확신합니다.

어떤 아이디어?


방금 다른 질문에 대해 꽤 일반적인 방법으로 답변했습니다 : stackoverflow.com/questions/14430655/…
tilgovi

답변:


231

이 바이올린을보세요

원본 : http://jsfiddle.net/brendanowen/uXbn6/8/

업데이트 : http://jsfiddle.net/animaxf/uXbn6/4779/

이것은 tree like structure각도를 사용하여 표시하는 방법에 대한 좋은 아이디어를 제공합니다 . HTML에서 재귀를 사용하는 것입니다!


94
출처를 밝히지 않습니까? 당신은 그 스레드에 게시물을 작성하고 지금 당신은 여기에 자신의 이름으로 URL을 게시하고 있습니까?
야누스 트롤 슨

5
CSS 섹션에 Twitter Bootstrap이 인라인되어 있지 않기 때문에 (최소한) 훨씬 더 빨리로드된다는 것을 제외하고는 동일한 버전입니다 (제 생각에). jsfiddle.net/brendanowen/uXbn6/8
KajMagnus

10
친구 당신은 당신의 소스를 명시해야합니다.
Ajax3.14 September

46
나는 사람들이 URL에 내 이름이 있다고 표명했습니다. 따라서 표절입니다! 불행히도 jsfiddle의 작동 방식입니다. 로그인 한 상태에서 포크를하면 사용자 이름이 유지됩니다. 내가 원래 URL에 연결했다고 말했습니다. 답이 틀린 경우 답을 내리십시오-이 시나리오에서는 내가 가진 백업 URL에 내 이름이 포함되어있는 것 한 가지로 답이 정확합니다.
ganaraj

5
방금 축소 및 확장 버튼을 버전에 추가했습니다. jsfiddle.net/uXbn6/639
jbaylina

77

부트 스트랩 CSS를 사용하는 경우 ...

Bootstrap "nav"목록을 기반으로 AngularJS에 대한 간단한 재사용 가능한 트리 컨트롤 (지시적)을 만들었습니다. 들여 쓰기, 아이콘 및 애니메이션을 추가했습니다. HTML 속성이 구성에 사용됩니다.

재귀를 사용하지 않습니다.

나는 그것을 angular-bootstrap-nav-tree 라고 불렀다 (캐치 이름, 당신은 생각하지 않습니까?)

이 예는 여기에 , 소스는 여기 .


1
아름답지만 Angular 1.0.x 브랜치에서는 작동하지 않습니다.
Danita

3
예, 새로운 애니메이션을 사용합니다. Angular 1.1.5가 필요합니다 (내 생각에?)
Nick Perkins

3
업데이트 : 지금은 어느 각도 1.1.5 또는 각도 1.2.0와 함께 작동, 또한 하나 Bootsrap 2 부트 스트랩 3와 함께 작동
닉 퍼킨스

1
참고로 Bower를 사용하는 경우 Nick은 이제 "bower search angular-bootstrap-nav-tree-save"및 "bower install angular-bootstrap-nav-tree --save"를 쉽게 설치할 수있게되었습니다.
arcseldon

2
@Nick Perkins-angular-bootstrap-nav-tree에 분기 / 노드 제거를위한 API가없는 이유를 설명 할 수 있습니다. 적어도 소스를 빠르게 검사하고 테스트 / 예제를 확인하면 해당 옵션이없는 것으로 보입니다. 이것은 중요한 누락입니까?
arcseldon

35

이와 같은 것을 만들 때 가장 좋은 해결책은 재귀 지시문입니다. 그러나 이러한 지시문을 만들면 AngularJS가 무한 루프에 빠진다는 것을 알 수 있습니다.

이를위한 해결책은 지시문이 컴파일 이벤트 중에 요소를 제거하고 링크 이벤트에서 수동으로 컴파일하고 추가하게하는 것입니다.

나는 이 글 에서 이것 에 대해 알아 내고이 기능 을 서비스로 추상화했다 .

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

이 서비스를 사용하면 트리 지시어 (또는 다른 재귀 지시어)를 쉽게 만들 수 있습니다. 다음은 트리 지시문의 예입니다.

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

데모는 이 Plunker 를 참조하십시오 . 나는이 솔루션을 가장 좋아합니다.

  1. HTML을 덜 깨끗하게 만드는 특별한 지시문이 필요하지 않습니다.
  2. 재귀 논리는 RecursionHelper 서비스로 추상화되므로 지시문을 깨끗하게 유지하십시오.

업데이트 : 사용자 지정 연결 기능에 대한 지원이 추가되었습니다.


1
이것은 너무 깔끔하고 강력 해 보입니다. 왜 이것이 이것이 angularjs에서 기본 동작이 아닌지 아십니까?
Paul

이와 같이 "컴파일"을 사용할 때 어떻게 범위에 추가 속성을 추가합니까? "컴파일"이 있으면 "링크"기능을 더 이상 사용할 수없는 것 같습니다.
Brian Kent

1
@ bkent314 이것에 대한 지원을 추가했습니다. 컴파일에서 반환하는 것과 같은 방식으로 연결 함수를 허용합니다. 또한 서비스를위한 Github 프로젝트를 만들었습니다.
Mark Lagendijk

@MarkLagendijk 매우 매끄 럽습니다! 지시문에서 재귀를 추상화하기 위해 많은 공감대가 필요합니다. 내가 본 모든 지시문은 그 논리가 혼합되어 있기 때문에 절망적으로 복잡해 보입니다.
acjay

이 유형의 솔루션에서 일부 데이터를 던질 것을 제안합니다. 예, 거의 모든 사람들이 재귀 지시문으로 트리를 구현하므로 쉽습니다. 그러나 ng-repeat $ digest만큼 속도가 느립니다. 수백 개의 노드에 도달하면 작동하지 않습니다.
Artemiy


15

다음은 재귀 지시문을 사용하는 예입니다. http://jsfiddle.net/n8dPm/ 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); 
            });
        };
    }
};
});

나는 이것을 실험하고 있었고, 나는 또한 transclusion을 사용하고 싶습니다. 가능하다고 생각하십니까?
L.Trabacchin


5

원본 소스 를 기반으로 한 또 다른 예제 는 샘플 트리 구조가 이미 배치되어 있고 (IMO 작동 방식을보기가 더 쉽다) 트리를 검색하는 필터입니다.

JSFiddle


4

많은 훌륭한 솔루션이지만, 한 가지 방식으로 또는 다른 방식으로 약간 복잡하게 느껴집니다.

@Mark Lagendijk의 awnser의 단순성을 재현 한 것을 만들려고했지만 지시문에서 템플릿을 정의하지 않고 "사용자"가 HTML로 템플릿을 만들 수있게했습니다 ...

https://github.com/stackfull/angular-tree-repeat 등에서 가져온 아이디어로 프로젝트를 만들었습니다 : https://github.com/dotJEM/angular-tree

다음과 같이 트리를 만들 수 있습니다.

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

다르게 구조화 된 나무에 대해 여러 지시문을 작성하는 것보다 나에게 더 깨끗한 것은 .... 본질적으로 위의 나무를 호출하는 것은 약간 잘못된 것입니다. 트리가 필요한 템플릿을 정의하십시오.

(스크립트 태그 기반 템플릿을 사용 하여이 작업을 수행 할 수는 있지만 여전히 실제 트리 노드 바로 밖에 있어야하며 여전히 약간의 느낌이 듭니다 ...)

또 다른 선택을 위해 여기를 떠났습니다 ...


업데이트 : 1.5 재귀 지시문은 이제 Angular에서 다소 기본적으로 지원됩니다. 이것은 dotjem / angular-tree의 사용 사례를 크게 좁 힙니다.
Jens

3

Angular-Ui-Tree를 사용 하여 Angular-Tree-DnD 샘플을 사용해 볼 수는 있지만 테이블, 그리드, 목록과의 호환성을 편집했습니다.

  • 에이블 드래그 앤 드롭
  • list에 대한 확장 함수 지시문 (next, prev, getChildren, ...)
  • 데이터를 필터링하십시오.
  • OrderBy (ver)

감사합니다. 드래그 앤 드롭이 필요했으며 이것이 유일한 해결책 인 것 같습니다!
Doug

2

@ganaraj을 기반으로의 대답은 , 그리고 @ dnc253의 대답은 , 난 그냥 트리 구조는 선택을 가진 추가, 삭제 및 기능을 편집하기위한 간단한 "지침"을했다.

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>

자바 스크립트 :

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

네 가능합니다. 여기서 질문은 아마도 Angular 1.x를 가정하지만 나중에 참조 할 수 있도록 Angular 2 예제를 포함하고 있습니다.

개념적으로해야 할 일은 재귀 템플릿을 만드는 것입니다.

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

그런 다음 트리 객체를 템플릿에 바인딩하고 Angular가 마법을 발휘하도록합니다. 이 개념은 Angular 1.x에도 적용 할 수 있습니다.

다음은 완전한 예입니다. http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


0

이를 위해 angular-recursion-injector를 사용할 수 있습니다 : https://github.com/knyga/angular-recursion-injector

컨디셔닝으로 깊이 심도 중첩을 수행 할 수 있습니다. 필요한 경우에만 재 컴파일하고 올바른 요소 만 컴파일합니다. 코드에 마법이 없습니다.

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

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

다른 솔루션보다 더 빠르고 간단하게 작동 할 수있는 것 중 하나는 "-재귀"접미사입니다.


0

트리 구조가 크면 재귀 템플릿을 렌더링 할 때 Angular (최대 1.4.x)가 매우 느려집니다. 이러한 제안을 많이 시도한 후 간단한 HTML 문자열을 만들어 ng-bind-html표시하는 데 사용 했습니다. 물론 이것은 각도 기능을 사용하는 방법이 아닙니다

베어 본 재귀 함수는 다음과 같습니다 (최소 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, ''));

템플릿에서는 다음 한 줄만 필요합니다.

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

이것은 Angular의 모든 데이터 바인딩을 무시하고 재귀 템플릿 메소드 시간의 일부만으로 HTML을 표시합니다.

다음과 같은 메뉴 구조 (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'}
        ]
       }

출력은 다음과 같습니다.

- 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

복잡하지 않습니다.

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

컨트롤러 코드 :

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


}]);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.