import {CollectionViewer, SelectionChange} from '@angular/cdk/collections'; import {FlatTreeControl} from '@angular/cdk/tree'; import {Component, Injectable} from '@angular/core'; import {BehaviorSubject, merge, Observable} from 'rxjs'; import {map} from 'rxjs/operators'; /** Flat node with expandable and level information */ export class DynamicFlatNode { constructor(public item: string, public level = 1, public expandable = false, public isLoading = false) {} } /** * Database for dynamic data. When expanding a node in the tree, the data source will need to fetch * the descendants data from the database. */ export class DynamicDatabase { dataMap = new Map([ ['Fruits', ['Apple', 'Orange', 'Banana']], ['Vegetables', ['Tomato', 'Potato', 'Onion']], ['Apple', ['Fuji', 'Macintosh']], ['Onion', ['Yellow', 'White', 'Purple']] ]); rootLevelNodes: string[] = ['Fruits', 'Vegetables']; /** Initial data from database */ initialData(): DynamicFlatNode[] { return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 0, true)); } getChildren(node: string): string[] | undefined { return this.dataMap.get(node); } isExpandable(node: string): boolean { return this.dataMap.has(node); } } /** * File database, it can build a tree structured Json object from string. * Each node in Json object represents a file or a directory. For a file, it has filename and type. * For a directory, it has filename and children (a list of files or directories). * The input will be a json object string, and the output is a list of `FileNode` with nested * structure. */ @Injectable() export class DynamicDataSource { dataChange = new BehaviorSubject([]); get data(): DynamicFlatNode[] { return this.dataChange.value; } set data(value: DynamicFlatNode[]) { this._treeControl.dataNodes = value; this.dataChange.next(value); } constructor(private _treeControl: FlatTreeControl, private _database: DynamicDatabase) {} connect(collectionViewer: CollectionViewer): Observable { this._treeControl.expansionModel.onChange.subscribe(change => { if ((change as SelectionChange).added || (change as SelectionChange).removed) { this.handleTreeControl(change as SelectionChange); } }); return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data)); } /** Handle expand/collapse behaviors */ handleTreeControl(change: SelectionChange) { if (change.added) { change.added.forEach(node => this.toggleNode(node, true)); } if (change.removed) { change.removed.slice().reverse().forEach(node => this.toggleNode(node, false)); } } /** * Toggle the node, remove from display list */ toggleNode(node: DynamicFlatNode, expand: boolean) { const children = this._database.getChildren(node.item); const index = this.data.indexOf(node); if (!children || index < 0) { // If no children, or cannot find the node, no op return; } node.isLoading = true; setTimeout(() => { if (expand) { const nodes = children.map(name => new DynamicFlatNode(name, node.level + 1, this._database.isExpandable(name))); this.data.splice(index + 1, 0, ...nodes); } else { let count = 0; for (let i = index + 1; i < this.data.length && this.data[i].level > node.level; i++, count++) {} this.data.splice(index + 1, count); } // notify the change this.dataChange.next(this.data); node.isLoading = false; }, 1000); } } /** * @title Tree with dynamic data */ @Component({ selector: 'tree-dynamic-example', templateUrl: 'tree-dynamic-example.html', styleUrls: ['tree-dynamic-example.css'], providers: [DynamicDatabase] }) export class TreeDynamicExample { constructor(database: DynamicDatabase) { this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); this.dataSource = new DynamicDataSource(this.treeControl, database); this.dataSource.data = database.initialData(); } treeControl: FlatTreeControl; dataSource: DynamicDataSource; getLevel = (node: DynamicFlatNode) => node.level; isExpandable = (node: DynamicFlatNode) => node.expandable; hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable; }