AngularJS : Expandable recursive tree table
You can consider using tree-grid.
demo: expandable grid
<tree-gird tree-data="tree_data"></tree-grid>
Provide a tree structured data, column definition and a property where you want to expand.
Provide a tree structured data, column definition and a property where you want to expand in your controller.
$scope.tree_data = [ {Name:"USA",Area:9826675,Population:318212000,TimeZone:"UTC -5 to -10", children:[ {Name:"California", Area:423970,Population:38340000, TimeZone:"Pacific Time", children:[ {Name:"San Francisco", Area:231,Population:837442,TimeZone:"PST"}, {Name:"Los Angeles", Area:503,Population:3904657,TimeZone:"PST"} ] }, {Name:"Illinois", Area:57914,Population:12882135,TimeZone:"Central Time Zone", children:[ {Name:"Chicago", Area:234,Population:2695598,TimeZone:"CST"} ] } ] }, {Name:"Texas",Area:268581,Population:26448193,TimeZone:"Mountain"}];
Optionally, you can define column definitions, properties on which you want to use expand and collapse
$scope.col_defs = [ { field: "Description"}, { field: "DemographicId", displayName: "Demographic Id"}, { field: "ParentId", displayName: "Parent Id"}, { field: "Area"}, { field: "Population"}, { field: "TimeZone", displayName: "Time Zone"}];$scope.expanding_property = "Name";
Add ng-if here
<div id="expanded-data" data-ng-if="childrenVisible">
and give your tree items a property which defines the visibility of their children.Set the property true or false (if you want false just dont add it by default) by default and implement a toggleChildren function which is called by a click on the toggleButton (the folder)
scope.toggleChildren = function () { scope.item.childrenVisible = !scope.item.childrenVisible;}
EDIT:// Now changing the folder (opened or closed)http://jsfiddle.net/8f3rL/35/
Use this table, with all CRUD operations and user friendly indentation :-
How to display tree data structure using Angularjs-1 and bootstral html table?
treeDS.html
<!doctype html><html><head> <title>Testbook: Admin App</title> <link rel="stylesheet" type="text/css" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <style> .level0{ text-indent : 0px; color : #428bca; } .level1{ text-indent : 20px; color : #428bca; } .level2{ text-indent : 40px; color : #428bca; } .level3{ text-indent : 60px; color : #428bca; } .level4{ text-indent : 80px; color : #428bca; } .level5{ text-indent : 100px; color : #428bca; } .level6{ text-indent : 120px; color : #428bca; } .hasDropdown{ color : #428bca ; } .noDropdown{ color : #000000; } div.tooltip-inner { text-align: center; -webkit-border-radius: 0px; -moz-border-radius: 0px; border-radius: 0px; margin-bottom: 6px; background-color: #505050; font-size: 14px; } </style></head><body ng-app="treeDataStructureApp" ng-controller="treeDataStructureCtrl"> <div class="container-fluid" style="padding-left : 25px;"> <br><br><br> <div class="row"> <div class="col-md-8 col-md-offset-2"> <span class="label label-default ng-binding" style="font-size : 25px"> Tree Data Structure Implementation Using Bootstrap Table </span> </div> </div> <br><br><br> <div class="row"> <div class="col-md-3"> <button ng-click="addNewNodeAtRootLevel()" type="button" class="btn btn-primary btn-sm">Add New Node</button> </div> </div> <br> <div class="row"> <div class="col-md-12"> <table class="table table-hover table-bordered" id="mytable" style="width: 90%;"> <caption></caption> <thead> <tr class="info"> <th data-toggle="tooltip" data-placement="top" title="click here to expand/close" ng-click="expandAll()"> Name <span ng-if="!showAll" class="glyphicon glyphicon-triangle-right" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="Help: expand all nodes"> </span> <span ng-if="showAll" class="glyphicon glyphicon-triangle-bottom" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="Help: "> </span> </th> <th>Id</th> <th>Operations</th> </tr> </thead> <tbody> <tr ng-repeat="node in nodesTableArr | filter : {isShow : true }" ng-class=" colorBackgroundOfNewnode(node)" > <td ng-class="[hasDropdown(node), selectIndentationClass(node)]"> <span ng-model="node.editable" contenteditable='node.isEditable' ng-click="toggleDropdown(node)"> {{node.name}}</span> </td> <td contenteditable='false'> {{node.id}} </td> <td contenteditable='false'> <span ng-click="addChildNode(node)" class="glyphicon glyphicon-plus" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="Help: add new node under this node edit its name and press save button,save button will show after editing its name"> </span> <span ng-click="deleteNode(node, 'concept')" class="glyphicon glyphicon-minus" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="Help: delete this node, all nodes under it will also be deleted"> </span> <span ng-click="editNode(node)" class="glyphicon glyphicon-edit" aria-hidden="true" data-toggle="tooltip" data-placement="top" title="Help: edit name and then press update button, update button will show, after editing its name"> </span> <button ng-click="saveNewNodeCB (node, false)" type="button" class="btn btn-primary btn-xs" ng-show="node.isSaveBtn">save </button> <button ng-click="updateNode(node)" type="button" class="btn btn-primary btn-xs" ng-show="node.isUpdateBtn">update </button> <span ng-medel="operationStatusMessage" ng-show="node.isShowMessage"> <small>{{operationStatusMessage}} </small> </span> </td> </tr> </tbody> </table> </div> </div> </div> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.min.js" type="text/javascript"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-route.min.js" type="text/javascript"></script> <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.1.js" type="text/javascript"></script> <script src="treeDS.js" type="text/javascript"></script> </body>
treeDS.js file
/** * Created by Aditya on 15/10/2015. */'use strict';
var app = angular.module('treeDataStructureApp',[]); app.controller('treeDataStructureCtrl', ['$scope',function ($scope){
// add new node at top level here $scope.isAddExamNode = false; $scope.addNewNodeAtRootLevel = function(){ $scope.isAddExamNode = true; var newNode = { name: "new node", id : "", level: 0, parent: "root", toggleStatus : false, parentId : -1, isShow: true, isEditable : false, childCount: 0, isSaveBtn : false, isShowMessage : false, type : "exam" }; $scope.nodesTableArr.unshift(newNode); }; // add new node. $scope.operationStatusMessage = ""; $scope.currentNodeSelected = {}; var uniqueIdForNewNodes = 0; $scope.addChildNode = function(node){ // add row to table. $scope.operationStatusMessage = ""; $scope.currentNodeSelected = node; for (var i = 0; i < $scope.nodesTableArr.length; i++) { if($scope.nodesTableArr[i].id == node.id){ $scope.nodesTableArr.splice(i + 1, 0, { name: "new node", id : uniqueIdForNewNodes, level: node.level + 1, parent: node.name, toggleStatus : false, parentId : node.id, isShow: true, isEditable : false, childCount: 0, isSaveBtn : false, isShowMessage : false, type : "nonExam" }); break; } } uniqueIdForNewNodes += 1; }; $scope.saveNewNode = function(node, isExamNode){ alert("You can send request on server to add this node here."); $scope.saveNewNodeCB(); }; $scope.saveNewNodeCB = function(){ $scope.resetOperationStatusMessage(); $scope.operationStatusMessage = "node Saved Successfully"; $scope.currentNodeSelected.isShowMessage = true; $scope.currentNodeSelected.isSaveBtn = false; // update node id in newly added node object, so that if we add new node under it, it has its valid parent id. for(var i = 0 ; i < $scope.nodesTableArr.length ; i++){ var node = $scope.nodesTableArr[i]; if(node == $scope.conceptNodeToAdd) node.id = response.data.node; } alert("send request to server here to save this node."); }; $scope.editNode = function(node){ $scope.nodeNameInOperation = node.name; $scope.operationStatusMessage = ""; $scope.currentNodeSelected = node; var nodeName = prompt("Please enter New node Name", node.name); if(nodeName != "" && nodeName != null && node.name != undefined && nodeName != node.name){ node.name = nodeName; if(node.id != ""){ node.isUpdateBtn = true; } else{ node.isSaveBtn = true; } } }; $scope.updateNode = function(node){ alert("You can send request on server to update this node."); $scope.updateNodeCB(); }; $scope.updateNodeCB = function(response){ $scope.resetOperationStatusMessage(); if(true){ $scope.currentNodeSelected.isShowMessage = true; $scope.operationStatusMessage = "node Updated Successfully"; $scope.currentNodeSelected.isUpdateBtn = false; } }; // nodeType = concept/nonConcept $scope.nodeToDelete = {}; $scope.deleteNode = function(node, nodeType){ $scope.nodeNameInOperation = node.name; $scope.nodeTypeForMessage = nodeType; $scope.nodeToDelete = node; $scope.operationStatusMessage = ""; $scope.currentNodeSelected = node; var message; if(node.id == ""){ for(var i = 0 ; i < $scope.nodesTableArr.length ; i++){ if($scope.nodesTableArr[i] == $scope.currentNodeSelected) $scope.nodesTableArr.splice(i, 1); } return 0 ; } var r = confirm("Warning! Be Carefull! On deletion all nodes under this node will be deleted.\nPress ok to delete node"); if (r == true) { $scope.deleteNodeCB(); message = "nodes deleted successfully."; } else { message = "process cancelled."; } $scope.curPageNo = 1; }; $scope.deleteNodeCB = function(){ $scope.resetOperationStatusMessage(); $scope.operationStatusMessage = "node deleted Successfully"; $scope.currentNodeSelected.isShowMessage = true; for(var i = 0 ; i < $scope.nodesTableArr.length ; i++){ if($scope.nodesTableArr[i] == $scope.currentNodeSelected) $scope.nodesTableArr.splice(i, 1); } }; $scope.resetOperationStatusMessage = function(){ for(var i = 0 ; i < $scope.nodesTableArr.length ; i++){ $scope.nodesTableArr[i].isShowMessage = false; } }; // concept nodes related operations ends here var node = ""; $scope.nodesTableArr = []; $scope.initializeNodeTreeTable = function(pChildArr){ var length = pChildArr.length || 0; for(var i = 0 ; i < length ; i++){ var exam = pChildArr[i]; var level = 0; var childCount = 0; if(exam.children && exam.children.length) childCount = exam.children.length; $scope.nodesTableArr.push({name : exam.text, id : exam._id, parent : "root", toggleStatus : false, parentId : -1, isShow : true, isEditable : false, level : 0, childCount : childCount, isSaveBtn : false, isUpdateBtn : false }); if(exam.children != undefined) $scope.initializeNodeTreeTableHelper(exam.children, exam.text, exam._id, level); } }; $scope.initializeNodeTreeTableHelper = function(pChildArr, pParentName, pPparentId, pLevel){ var isShowNode = false; pLevel = pLevel + 1 ; for(var i = 0 ; i < pChildArr.length ; i++){ var node = pChildArr[i]; var childCount = node.children != undefined ? node.children.length : 0 $scope.nodesTableArr.push({name : node.text, id : node._id, parent : pParentName, toggleStatus : false, parentId : pPparentId, isShow : isShowNode, isEditable : false, level : pLevel, childCount : childCount, isSaveBtn : false, isUpdateBtn : false }); if(node.children != undefined) $scope.initializeNodeTreeTableHelper(node.children, node.text, node._id, pLevel) } }; $scope.selectedExam = ""; $scope.renderNodeTreeForExam = "selectedExamNode"; $scope.renderNodeTreeForExam = function(exam){ $scope.nodesTableArr = []; $scope.selectedExam = exam; var selectedExamArr = [exam]; if($scope.selectedExam == "allExams") selectedExamArr = $scope.allExams $scope.getAllNodes(selectedExamArr); }; // view related functions. $scope.selectIndentationClass = function(node){ return 'level' + node.level; }; $scope.hasDropdown = function(node){ if(node.childCount > 0) return "hasDropdown"; else return "noDropdown"; }; $scope.colorBackgroundOfNewNode = function(node){ if(node.id == ""){ return "success"; } }; $scope.toggleStatus = false; $scope.toggleDropdown = function(node){ node.toggleStatus = node.toggleStatus == true ? false : true; $scope.toggleStatus = node.toggleStatus; $scope.toggleDropdownHelper(node.id, $scope.toggleStatus ); }; $scope.toggleDropdownHelper = function(parentNodeId, toggleStatus ){ for(var i = 0 ; i < $scope.nodesTableArr.length ; i++) { node = $scope.nodesTableArr[i]; if(node.parentId == parentNodeId) { if(toggleStatus == false) $scope.toggleDropdownHelper(node.id, toggleStatus); $scope.nodesTableArr[i].isShow = toggleStatus; } } }; $scope.showAll = false; $scope.expandAll = function(){ var i = 0; $scope.showAll = $scope.showAll == true ? false : true; if($scope.showAll) { for (i = 0; i < $scope.nodesTableArr.length; i++) $scope.nodesTableArr[i]['isShow'] = true; } else { for (i = 0; i < $scope.nodesTableArr.length; i++) if($scope.nodesTableArr[i]['level'] != 0) $scope.nodesTableArr[i]['isShow'] = false; } }; $scope.initializeNodeTreeTable(sampleData.data)}]); var sampleData = { "data": [ { "_id": "557569e82a39650f65425104", "depth": 0, "text": "SBI Clerk", "exam": "SBI Clerk", "children": [ { "_id": "55c2dee72a3965432eaac6a7", "depth": 1, "text": "Quantitative Aptitude", "exam": "SBI Clerk", "children": [ { "_id": "55c2dee72a3965432eaac6a8", "depth": 2, "text": "Simplification", "exam": "SBI Clerk", "children": [ { "_id": "55c2dee72a3965432eaac6a9", "depth": 3, "text": "Bodmas Rule", "exam": "SBI Clerk", "type": "topic", "par": "Bodmas Rule" }, { "_id": "55c2dee72a3965432eaac6aa", "depth": 3, "text": "Surds And Indices", "exam": "SBI Clerk", "type": "topic", "par": "Surds And Indices" }, { "_id": "55c2dee82a3965432eaac6ab", "depth": 3, "text": "Approx Value", "exam": "SBI Clerk", "type": "topic", "par": "Approx Value" }, { "_id": "55c2dee82a3965432eaac6ac", "depth": 3, "text": "Percennodee", "exam": "SBI Clerk", "type": "topic", "par": "Percennodee" } ], "type": "chapter", "par": "Simplification" } ], "type": "subject", "par": "Quantitative Aptitude" } ], "type": "exam", "par": ""} ]};
jsFiddle : Working Example