Is it possible to make a Tree View with Angular?
Asked Answered
G

14

178

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?

Gardener answered 7/8, 2012 at 21:38 Comment(1)
I just answered this in a pretty generic way over on a different question: #14431155Carrillo
U
236

Have a look at this fiddle

Original: http://jsfiddle.net/brendanowen/uXbn6/8/

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

This should give you a good idea of how to display a tree like structure using angular. It is kind of using recursion in html!

Uncloak answered 8/8, 2012 at 8:56 Comment(18)
why not state your source? you wrote a post in that thread, and now you're posting a url here with your own name in it?Kampmeier
Here is an identical version (I think), except that it loads much faster (for me at least), since it doesn't have Twitter Bootstrap inlined in the CSS section. jsfiddle.net/brendanowen/uXbn6/8Ecstatic
This simply binds a new controller instance to a nested element. Is it possible to parameterize the controller somehow? And is it possible to chain these controllers so that you get a parent-child chain in your business logic?Rossini
Would there be any benefit of using a directive here?Keramics
Summary: create an ng-template that ng-include itself, each include going down the branch. Brilliant!Drusie
Here's a directive for it logically-thinking.blogspot.in/2013/06/…Homeopathy
@JonathanAbrams The solution in pretty good, but it renders an "<ul>" with no child. I tried to tweak it but i could not make a logic avoid render an empty "<ul>". Is it possible to do it with ng-include or this feature need to be inside and directive for DOM manipulations?Anesthesia
dude you should state your source.Ottinger
I was really tired of people constantly commenting on this that the URL has my name in it ( and therefore it is plagiarism! ). That is unfortunately how jsfiddle works. If you fork something while you are logged in it retains your username. Having said that I have now linked to the original URL. Downvote an answer if it is wrong - The answer happens to be correct in this scenario with the one thing that backup URL I had seems to contain my name in it.Uncloak
I just added the collapse and expand button to your version: jsfiddle.net/uXbn6/639Drinking
Try #14431155 for some good solutionsRossini
How would you add 'delete current node' button instead of 'delete [child] nodes'? I've tried several ways, but neither works very well.Judejudea
Super simple solution which really shows the power of angular, I wanted to try and make it even more simple though, as i felt it was a bit yuk having to rely on ng-include/template cache to perform this when it should just be possible directly in the DOM... which led me to create: github.com/dotJEM/angular-treeGrayce
I don't understand this example. All I get is infinite recursion. It seems like each node isn't getting it's own scope if the tree is generated at runtime.Dissimilate
Forked @JohnHenckel 's code, and changed a delete button to delete **current ** node instead of children's node. jsfiddle.net/yoshiokatsuneo/12bfpvnLAccouchement
DOES NOT BIND DATA THOUGH, thus defeating the purpose of using angular vs. a server-side templating languageFantasm
It there any way to get the deph using this method ?Unmistakable
How can we have delete option with tree node in above exampleKrumm
H
77

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.

Hiles answered 18/8, 2013 at 16:54 Comment(11)
It's beautiful, but be warned it doesn't work on Angular 1.0.x branch.Xeres
Yes, it uses the new animation stuff...requires Angular 1.1.5 ( I think? )Hiles
UPDATE: it now works with either Angular 1.1.5 or Angular 1.2.0, and also works with either Bootsrap 2 or Bootstrap 3Hiles
Hi, Nick, like your directive. I am looking for something similar, but I need it to have hover drop functionality. How to go about this...Linseed
@NickPerkins : I am trying to go through your github example. I dont understand how is treeData changing when you click on the collapse, expand icon. I dont see any ng-click etc. that is changing the data in the $scope.treeData. Am I missing something?Excelsior
@NickPerkins Hey Nick, nice and clean tree. What can I do to integrate check-boxes?Hershel
THanks. if in all I have 30K nodes - is it ok to bring it FE in advance?Devilish
FYI only, if using Bower, Nick has now made this available for easy install - "bower search angular-bootstrap-nav-tree", and "bower install angular-bootstrap-nav-tree --save" and you are done.Chery
@Nick Perkins - please can you explain why your angular-bootstrap-nav-tree has no API for removing a Branch / Node. At least, from a quick inspection of the source, and checking your test / examples there does not appear to be that option. This is a critical omission, surely?Chery
Needs multi node selectR
@NickPerkins I used your code in my project it works great. But it slowed down when i added some 800 records. I think its due to the $watch expression?Dianthus
S
35

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.

Suint answered 28/9, 2013 at 10:29 Comment(7)
this seems to be so neat and powerful, any idea why this isn't a default behavior in angularjs?Bullock
When using "compile" like this, how does one add additional attributes to the scope? The "link" function seems no longer available once "compile" is there...Demythologize
@bkent314 I added support for this. It now accepts linking functions in the same way as compile can return them. I also created a Github project for the service.Suint
@MarkLagendijk Very, very slick! You deserve many upvotes for abstracting the recursion out of the directive. All of the directives I've seen look hopelessly complicated with that logic mixed in. Is there a way to make your RecursionHelper work with transclusion?Intersperse
I really do suggest that you throw some data at this type of solution - yes, almost everybody implements tree with recursive directives, it's easy. But it's extremely slow as ng-repeat $digest's - once you get to hundreds of nodes, this doesn't perform.Chopine
naive question, why isn't the simple jsbin.com/rapovi/8/edit?html,js,output working? of course your example works jsbin.com/rapovi/10/edit?html,js,outputHeribertoheringer
I had my own tree directive, switch I used for 3 years in my prev projects. and now I discovered that it has problems! so I took that your RecursionHelper, paste my template and its works! Thanks man!Gully
M
18

angular-ui-tree seems to do a good job for me

Masonry answered 6/5, 2014 at 15:46 Comment(0)
S
15

Here is an example using a recursive directive: http://jsfiddle.net/n8dPm/ Taken from 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); 
            });
        };
    }
};
});
Scorify answered 27/5, 2013 at 21:28 Comment(1)
i was experimenting with this, and i'd like to use transclusion too, do you think it's possible ?Enlarger
A
12

This one seems a bit more complete: https://github.com/dump247/angular.tree

Anaxagoras answered 4/3, 2013 at 16:53 Comment(0)
D
5

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

Demonology answered 23/3, 2013 at 23:32 Comment(0)
G
4

So many great solutions, but I feel they all in one way or another over-complicate things a bit.

I wanted to create something that recreated the simplicity of @Mark Lagendijk's awnser, but without it defining a template in the directive, but rather would let the "user" create the template in HTML...

With ideas taken from https://github.com/stackfull/angular-tree-repeat etc... I ended up with creating the project: https://github.com/dotJEM/angular-tree

Which allows you to build your tree like:

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

Which to me is cleaner than having to create multiple directives for differently structured trees.... In essence calling the above a tree is a bit false, it picks much more off from @ganaraj's awnser of "recursive templates", but allows us to define the template where we need the tree.

(you could do that with a script tag based template, but it still has to sit right outside the actual tree node, and it still just feels a bit yuk...)

Left here for just another choice...

Grayce answered 9/6, 2014 at 16:20 Comment(1)
UPDATE: As of 1.5 recursive directives are now somewhat natively supported in Angular. This narrows down the use cases for dotjem/angular-tree a great deal.Grayce
C
3

You can try with Angular-Tree-DnD sample with Angular-Ui-Tree, but i edited, compatibility with table, grid, list.

  • Able Drag & Drop
  • Extended function directive for list (next, prev, getChildren,...)
  • Filter data.
  • OrderBy (ver)
Churinga answered 29/5, 2015 at 16:31 Comment(1)
Thank-you. I needed the Drag & Drop, and this appears to be the only solution with that!Erdei
A
2

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://mcmap.net/q/136143/-recursion-in-angular-directives */
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: '=',
        }
    }
});
Accouchement answered 30/11, 2015 at 6:41 Comment(0)
D
0

Yes it definitely possible. The question here probably assumes Angular 1.x, but for future reference I am including an Angular 2 example:

Conceptually all you have to do is create a recursive template:

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

You then bind a tree object to the template and let Angular work its magic. This concept is obviously applicable to Angular 1.x as well.

Here is a complete example: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

Damp answered 30/5, 2015 at 20:55 Comment(0)
H
0

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.

Hypomania answered 12/6, 2015 at 10:22 Comment(0)
P
0

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
Ponceau answered 16/9, 2015 at 21:44 Comment(0)
L
-3

Not complicated.

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

controller code:

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


}]);
Lucio answered 20/10, 2015 at 4:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.