是否可以用Angular制作树状视图?


177

我希望在Web应用程序中以树状结构显示数据。我希望将Angular用于此任务。

看起来ng-repeat可以让我遍历节点列表,但是当给定节点的深度增加时,该如何嵌套呢?

我尝试了以下代码,但是HTML的自动转义阻止了此操作。另外,结束ul标签放在错误的位置。

我很确定我将完全以错误的方式解决这个问题。

有任何想法吗?


:我刚才已经回答这一个非常通用的方式在不同的问题上stackoverflow.com/questions/14430655/...
tilgovi

Answers:


231

看看这个小提琴

原文:http//jsfiddle.net/brendanowen/uXbn6/8/

更新:http : //jsfiddle.net/animaxf/uXbn6/4779/

这应该使您对如何显示tree like structure使用角度有个好主意。这是在html中使用递归的一种!


94
为什么不注明您的出处?您在该主题中撰写了一篇文章,现在您要在此处发布一个带有您自己名字的网址吗?
Janus Troelsen 2012年

5
这是一个相同的版本(我认为),除了它加载速度更快(至少对我而言),因为它在CSS部分中没有内嵌Twitter Bootstrap。jsfiddle.net/brendanowen/uXbn6/8
KajMagnus 2012年

10
老兄,您应该说明您的出处。
Ajax3.14 2013年

46
我真的厌倦了人们不断评论这个URL上有我的名字(因此这是it窃!)。不幸的是,这就是jsfiddle的工作方式。如果您在登录时进行了分叉,它将保留您的用户名。话虽如此,我现在已经链接到原始URL。如果答案错误,请否决答案-在这种情况下,答案是正确的,有一件事是我备份的URL似乎包含我的名字。
ganaraj 2013年

5
我刚刚在您的版本中添加了“折叠并展开”按钮:jsfiddle.net/uXbn6/639
jbaylina 2013年

77

如果您使用的是Bootstrap CSS ...

我已经基于Bootstrap“ nav”列表为AngularJS创建了一个简单的可重用树控件(指令)。我添加了额外的缩进,图标和动画。HTML属性用于配置。

它不使用递归。

我称它为angular-bootstrap-nav-tree(俗称,您不觉得吗?)

有一个例子在这里,源是在这里


1
它很漂亮,但要警告它在Angular 1.0.x分支上不起作用。
Danita 2013年

3
是的,它使用了新的动画内容...需要Angular 1.1.5(我认为吗?)
Nick Perkins

3
更新:它现在可以与Angular 1.1.5或Angular 1.2.0一起使用,并且还可以与Bootsrap 2或Bootstrap 3一起使用
Nick Perkins

1
仅供参考,如果使用Bower,Nick现在已可以轻松安装该工具-“ bower search angular-bootstrap-nav-tree”和“ bower install angular-bootstrap-nav-tree --save”已完成。
arcseldon 2014年

2
@Nick Perkins-请您能解释一下为什么您的angular-bootstrap-nav-tree没有用于删除分支/节点的API。至少,从快速检查源代码开始,然后检查您的测试/示例,似乎没有该选项。当然,这是一个关键的遗漏?
arcseldon 2014年

35

进行此类操作时,最好的解决方案是递归指令。但是,当您做出这样的指令时,您会发现AngularJS陷入了一个无限循环。

解决方案是让指令在compile事件期间删​​除该元素,然后手动进行编译并将其添加到link事件中。

我在此线程中发现了这一点,并将此功能抽象为服务

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

看到这个柱塞的演示。我最喜欢此解决方案,因为:

  1. 您不需要特殊的指令,这会使您的html不太干净。
  2. 递归逻辑被抽象到RecursionHelper服务中,因此您可以保持指令整洁。

更新:添加了对自定义链接功能的支持。


1
这似乎是如此整洁和强大,可以知道为什么这不是angularjs中的默认行为吗?
保罗

当使用这样的“编译”时,如何将其他属性添加到作用域?一旦存在“编译”,“链接”功能就不再可用...
Brian Kent

1
@ bkent314我为此添加了支持。现在,它接受链接函数的方式与编译可以返回它们的方式相同。我还为该服务创建了一个Github项目。
2014年

@MarkLagendijk非常非常光滑!您应该为从指令中提取递归提供许多建议。我所见过的所有指令看起来都与该逻辑混在一起显得无可救药。有没有办法使您的RecursionHelper与包含一起工作?
2014年

我确实建议您在这种解决方案中投入一些数据-是的,几乎每个人都使用递归指令实现树,这很容易。但这非常慢,就像ng-repeat $ digest一样-一旦到达数百个节点,此操作将无法执行。
Artemiy 2014年


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

我正在对此进行试验,我也想使用包含,您认为有可能吗?
L.Trabacchin



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>

对我来说,这比必须为结构不同的树创建多个指令更干净...。本质上,在上面调用一棵树有点错误,它从@ganaraj的“递归模板”遮篷中获得了更多收益,但是允许我们在需要树的位置定义模板。

(您可以使用基于脚本标签的模板来做到这一点,但是它仍然必须坐在实际的树节点的外面,并且仍然感觉有点......)

留在这里只是另一个选择...


更新:从1.5版本开始,Angular现在已本地支持递归指令。这极大地缩小了dotjem / angular-tree的用例范围。
延斯

3

您可以尝试将Angular-Ui-Tree 与Angular-Tree-DnD示例一起使用,但我已对其进行了编辑,与表格,网格,列表兼容。

  • 能够拖放
  • 列表的扩展功能指令(next,prev,getChildren等)
  • 过滤数据。
  • OrderBy(版本)

谢谢。我需要拖放,这似乎是唯一的解决方案!
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>

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

是的,绝对有可能。这里的问题可能假设为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发挥其魔力。这个概念显然也适用于Angular1.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用于显示它。当然,这不是使用Angular功能的方法

此处显示了一个基本的递归函数(使用最少的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"
    }];


}]);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.