Is it possible to make a Tree View with Angular?

2019-01-01 09:55发布

I'm looking to display data in a tree structure in a web app. I was hoping to use Angular for this task.

Looks like ng-repeat will allow me to iterate through a list of nodes, but how can I then do nesting when a given node's depth increases?

I tried the following code, but the auto-escaping of the HTML is preventing this from working. Plus, the end ul tag is in the wrong place.

I'm pretty sure that I'm going about this problem entirely the wrong way.

Any ideas?

标签: angularjs
14条回答
与君花间醉酒
2楼-- · 2019-01-01 10:13

When making something like this the best solution is an recursive directive. However, when you make such an directive you find out that AngularJS gets into an endless loop.

The solution for this is to let the directive remove the element during the compile event, and manually compile and add them in the link events.

I found out about this in this thread, and abstracted this functionality into a service.

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

With this service you can easily make a tree directive (or other recursive directives). Here is an example of an tree directive:

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

See this Plunker for a demo. I like this solution best because:

  1. You don't need an special directive which makes your html less clean.
  2. The recursion logic is abstracted away into the RecursionHelper service, so you keep your directives clean.

Update: Added support for a custom linking functions.

查看更多
不再属于我。
3楼-- · 2019-01-01 10:13

You can use angular-recursion-injector for that: https://github.com/knyga/angular-recursion-injector

Allows you to do unlimited depth nesting with conditioning. Does recompilation only if needed and compiles only right elements. No magic in code.

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

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

One of the things that allows it to work faster and simpler then the other solutions is "--recursion" suffix.

查看更多
余欢
4楼-- · 2019-01-01 10:15

Another example based off the original source, with a sample tree structure already in place (easier to see how it works IMO) and a filter to search the tree:

JSFiddle

查看更多
牵手、夕阳
5楼-- · 2019-01-01 10:18

Based on @ganaraj 's answer, and @dnc253 's answer, I just made a simple "directive" for the tree structure having selecting, adding, deleting, and editing feature.

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: '=',
        }
    }
});
查看更多
美炸的是我
6楼-- · 2019-01-01 10:19

If you are using Bootstrap CSS...

I have created a simple re-usable tree control (directive) for AngularJS based on a Bootstrap "nav" list. I added extra indentation, icons, and animation. HTML attributes are used for configuration.

It does not use recursion.

I called it angular-bootstrap-nav-tree ( catchy name, don't you think? )

There is an example here, and the source is here.

查看更多
梦寄多情
7楼-- · 2019-01-01 10:20

When the tree structure is large, Angular (up to 1.4.x) becomes very slow at rendering a recursive template. After trying a number of these suggestions, I ended up creating a simple HTML string and using ng-bind-html to display it. Of course, this is not the way to use Angular features

A bare-bones recursive function is shown here (with minimal 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, ''));

In the template, it only needs this one line:

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

This bypasses all of Angular's data binding and simply displays the HTML in a fraction of the time of the recursive template methods.

With a menu structure like this (a partial file tree of a Linux file system):

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'}
        ]
       }

The output becomes:

- 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
查看更多
登录 后发表回答