Drill down with folders and selectable items in Angular.
Includes: Dynamic drill-down, select-all functionality, and breadcrumbs.
A Pen by Anton Vishnyak on CodePen.
Drill down with folders and selectable items in Angular.
Includes: Dynamic drill-down, select-all functionality, and breadcrumbs.
A Pen by Anton Vishnyak on CodePen.
<body ng-app="app"> | |
<div ng-controller="test"> | |
<div class="panel panel-success" style="width: 400px;height:500px"> | |
<drill-down src="tree" select-all selected="selectedItems" get-path="getPath" get-display-name="getDisplayName"></drill-down> | |
</div> | |
</div> | |
</body> |
angular.module('app', []).controller('test', ['$scope', function($scope) { | |
$scope.tree = [{ | |
name: "Admin", | |
scope: "" | |
}, { | |
name: "Manager", | |
scope: "California" | |
}, { | |
name: "Anton", | |
scope: "California|north" | |
}, { | |
name: "Mickey", | |
scope: "California|north" | |
}, { | |
name: "Laura", | |
scope: "California|south" | |
}, { | |
name: "Laura", | |
scope: "California|really long name|southern territory|Sietch Tabr" | |
}]; | |
$scope.selectedItems = []; | |
$scope.getPath = (i) => i.scope; | |
$scope.getDisplayName = (i) => i.name; | |
}]); | |
angular.module('app').directive('drillDown', drillDownDirective); | |
drillDownDirective.$inject = []; | |
function drillDownDirective() { | |
let scope = { | |
delimeter: '@', | |
src: '=', | |
selected: '=', | |
getPath: '&', | |
getDisplayName: '&' | |
}; | |
return { | |
restrict: 'E', | |
scope, | |
bindToController: true, | |
link, | |
template: ` | |
<div class="dd-wrapper"> | |
<div class="dd-breadcrumb-wrapper"> | |
<ul class="breadcrumb"> | |
<li><a href="#" ng-click="selectPath(0)"><span class="fa fa-folder-open"></span></a></li> | |
<li ng-repeat="p in selectedPathParts" ng-class="{ 'active': $last }"> | |
<a href="#" ng-click="selectPath($index + 1)">{{:: p }}</a> | |
</li> | |
</ul> | |
<div class="list-group-item" ng-if="selectedItems.length > 0"><i>{{ selectedItems.length }} selected</i></div> | |
<div class="list-group-item" ng-if="selectedItems.length == 0"> | |
<a href="#" ng-click="selectAllNodes()">Select All</a> | |
</div> | |
</div> | |
<div class="dd-menu-wrapper"> | |
<ul class="dd-menu nav"> | |
<li ng-repeat="item in items" ng-class="{ 'dd-parent': isParent(item) }"> | |
<a href="#" ng-click="selectItem(item)"> | |
{{:: item.name }} | |
<i ng-if="!isParent(item) && item.selected" class="fa fa-check pull-right"></i> | |
<i ng-if="isParent(item)" class="fa fa-chevron-right pull-right"></i> | |
</a> | |
</li> | |
</ul> | |
</div> | |
</div> | |
` | |
}; | |
function link(scope, elem, attrs) { | |
const delimeter = scope.delimeter || '|'; | |
scope.tree = {}; | |
scope.selectedPath = ''; | |
scope.selectedPathParts = []; | |
scope.items = []; | |
scope.selectedItems = []; | |
scope.selectAll = _.has(attrs, 'selectAll'); | |
// Exposed functions | |
scope.isParent = isParent; | |
scope.selectItem = selectItem; | |
scope.selectPath = selectPath; | |
scope.selectAllNodes = selectAllNodes; | |
// Handle data | |
scope.$watch(() => scope.src, (n) => { | |
scope.tree = buildTree(scope.src, delimeter); | |
}); | |
scope.$watch(() => scope.selectedPath, () => { | |
scope.items.splice(0, scope.items.length); | |
scope.selectedPathParts = scope.selectedPath.split(delimeter); | |
_.forEach(getItems(scope.selectedPath), (item) => { | |
scope.items.push(item); | |
}); | |
}); | |
function selectPath(index) { | |
scope.selectedPathParts.splice(index, scope.selectedPathParts.length); | |
scope.selectedPath = index === 0 ? '' : scope.selectedPathParts.join(delimeter); | |
scope.selectedItems.splice(0, scope.selectedItems.length); | |
} | |
function getItems(path) { | |
debugger; | |
let nodes = _(scope.tree[path] || []) | |
.map((item, i) => { | |
return { | |
path: path, | |
name: scope.getDisplayName()(item), | |
hasChildren: false, | |
index: i, | |
selected: false | |
}; | |
}) | |
.sortBy((i) => i.name) | |
.value(), | |
edges = _(scope.tree) | |
.keys() | |
.filter((i) => { | |
return path.length == 0 && i.length > 0 || i.startsWith(path + delimeter); | |
}) | |
.map((i) => { | |
let nextSegment = i.indexOf(delimeter, path.length + 1); | |
return i.substring(path.length === 0 ? 0 : path.length + 1, nextSegment === -1 ? undefined : nextSegment); | |
}) | |
.sortBy() | |
.uniq() | |
.map((e) => { | |
return { | |
path: path, | |
name: e, | |
hasChildren: true | |
} | |
}) | |
.value(); | |
return _.union(edges, nodes); | |
} | |
function selectAllNodes() { | |
let nodes = _.filter(scope.items, (i) => i.hasChildren === false); | |
if (nodes.length > 0) { | |
let treeItem = scope.tree[scope.selectedPath]; | |
scope.selectedItems.splice(0, scope.selectedItems.length); | |
_.forEach(treeItem, (i) => { | |
scope.selectedItems.push(i); | |
}); | |
_.forEach(nodes, (n) => { | |
n.selected = true; | |
}); | |
} | |
} | |
function selectItem(item) { | |
if (item.hasChildren) { | |
scope.selectedPath = item.path === '' ? item.name : item.path + delimeter + item.name; | |
scope.selectedItems.splice(0, scope.selectedItems.length); | |
} else { | |
// Toggle selection | |
let i = _.findIndex(scope.selectedItems, (p) => { | |
return _.eq(p, scope.tree[item.path][item.index]); | |
}); | |
if (i >= 0) { | |
scope.selectedItems.splice(i, 1); | |
item.selected = false; | |
} else { | |
scope.selectedItems.push(scope.tree[item.path][item.index]); | |
item.selected = true; | |
} | |
} | |
} | |
function isParent(item) { | |
return item.hasChildren; | |
} | |
// Helper functions | |
function buildTree(src, delimeter) { | |
return _.reduce(src, (acc, nxt) => { | |
let path = scope.getPath()(nxt), | |
node = acc[path]; | |
if (_.isUndefined(node)) { | |
node = acc[path] = []; | |
} | |
node.push(nxt); | |
return acc; | |
}, {}); | |
} | |
} | |
} |
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-animate.js"></script> |
@use cssnext; | |
@use postcss-nested; | |
.dd-wrapper { | |
position: relative; | |
height: 100%; | |
width: 100%; | |
display: flex; | |
flex-direction: column; | |
& ul, & li { | |
list-style: none; | |
} | |
& .breadcrumb { | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
margin-bottom: 0; | |
border-radius: 0; | |
& li { | |
display: inline; | |
&:nth-child(n+2) > a { | |
display: none; | |
} | |
&:nth-child(n+2):after { | |
position: relative; | |
left: -5px; | |
content: "\2026"; | |
} | |
&:nth-last-child(-n+2) a { | |
display: inline; | |
} | |
&:nth-last-child(-n+2):after { | |
display: none; | |
} | |
} | |
} | |
& .dd-menu-wrapper { | |
overflow: scroll; | |
} | |
& .dd-menu { | |
& ul { | |
margin: 0; | |
position: absolute; | |
top: 0; | |
right: 0; | |
} | |
& a { | |
display: block; | |
} | |
} | |
} |
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" /> | |
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" /> |