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

178

我希望在Web应用程序中以树形结构显示数据。我希望使用Angular完成这个任务。

看起来ng-repeat将允许我遍历节点列表,但是当给定节点的深度增加时,如何进行嵌套呢?

我尝试了以下代码,但HTML的自动转义导致它无法正常工作。此外,结束ul标签的位置也不正确。

我很确定自己完全错误地处理了这个问题。

有什么想法吗?


我刚在另一个问题上以一种相当通用的方式回答了这个问题:https://dev59.com/uWYq5IYBdhLWcg3wfwxx#29736154 - tilgovi
14个回答

236

94
为什么不声明你的来源?你在那个帖子中写了一篇文章,现在在这里发布一个带有你自己名字的链接? - Janus Troelsen
5
这是一个基本相同的版本(我认为),与原版本不同的是,它加载速度更快(至少对我来说),因为它没有在CSS部分内联Twitter Bootstrap。http://jsfiddle.net/brendanowen/uXbn6/8/ - KajMagnus
10
伙计,你应该注明你的来源。 - Ajax3.14
47
我真的厌倦了人们不断评论这个URL中包含我的姓名(因此是剽窃!)。不幸的是,这就是jsfiddle的工作方式。如果在登录状态下fork某个东西,它会保留您的用户名。话虽如此,我现在已经链接到原始URL。如果答案错误,请进行downvote-在这种情况下,答案恰好是正确的,唯一的问题是我的备份URL似乎包含我的名字。 - ganaraj
5
我刚刚为你的版本添加了折叠和展开按钮:http://jsfiddle.net/uXbn6/639/ - jbaylina
显示剩余13条评论

77
如果您正在使用 Bootstrap CSS...
我创建了一个简单可重用的树形控件(指令)供 AngularJS 使用,它基于 Bootstrap "nav" 列表。为其添加了额外的缩进、图标和动画效果,并使用 HTML 属性进行配置。
它不使用递归。
我将其称为 angular-bootstrap-nav-tree(很有吸引力的命名,你不觉得吗?)。
这里有一个示例here,源代码在here

1
很漂亮,但请注意它不适用于Angular 1.0.x分支。 - Danita
3
是的,它使用了新的动画功能...需要Angular 1.1.5(我想是这个版本吧?) - Nick Perkins
3
更新:现在它可与Angular 1.1.5或Angular 1.2.0一起使用,并且还可与Bootstrap 2或Bootstrap 3一起使用。 - Nick Perkins
1
仅供参考,如果使用Bower,Nick现在已经使其可轻松安装 - “bower search angular-bootstrap-nav-tree”,然后“bower install angular-bootstrap-nav-tree --save”,就完成了。 - arcseldon
2
@Nick Perkins - 请问您能否解释一下为什么您的angular-bootstrap-nav-tree没有删除分支/节点的API。至少从快速检查源代码和检查测试/示例来看,似乎没有这个选项。这是一个重要的遗漏,不是吗? - arcseldon
显示剩余6条评论

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 的默认行为? - Boycott Russia
当使用类似"compile"的方式时,如何向作用域添加其他属性?一旦出现"compile",似乎就无法再使用"link"函数了... - Brian Kent
1
@bkent314,我已经添加了对此的支持。现在它可以像编译一样接受链接函数。我还为该服务创建了一个Github项目。 - Mark Lagendijk
@MarkLagendijk非常、非常地漂亮!你应该因为将递归抽象出指令而获得许多赞。我看到的所有指令都带有混杂在其中的逻辑,看起来都无望复杂了。你的RecursionHelper有没有办法与转换一起工作? - acjay
我真的建议你向这种解决方案投入一些数据 - 是的,几乎每个人都使用递归指令来实现树形结构,这很容易。但是随着节点数量的增加,它变得极其缓慢,就像ng-repeat $digest一样 - 一旦你达到数百个节点,它就无法执行。 - Artemiy
显示剩余2条评论

18

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

12

5
基于原始来源的另一个示例,已经放置了一个样本树结构(更易于理解其工作原理),并且有一个过滤器用于搜索树:

JSFiddle


4
这里有很多优秀的解决方案,但我觉得它们在某种程度上都有点过于复杂了。我想创造一个重现@Mark Lagendijk答案简洁性的东西,但是不用在指令中定义模板,而是让“用户”在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的用例范围。 - Jens

3
您可以尝试使用 Angular-Tree-DnD 和 Angular-Ui-Tree 的示例,但我进行了编辑以与表格、网格和列表兼容。
  • 可拖放
  • 扩展功能指令列表(下一个,上一个,获取子项等)
  • 数据过滤器。
  • 排序(版本)

谢谢。我需要拖放功能,这似乎是唯一带有该功能的解决方案! - 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://dev59.com/uWYq5IYBdhLWcg3wfwxx#14657310 */
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: '=',
        }
    }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接