Last active
May 25, 2021 08:32
-
-
Save Bludwarf/17a20ebe5e38aa34e8bfe5a6752e50ea to your computer and use it in GitHub Desktop.
Angular Material Tree Dynamic based on https://stackblitz.com/edit/material-tree-dynamic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Injectable} from '@angular/core'; | |
import {FlatTreeControl} from '@angular/cdk/tree'; | |
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections'; | |
import {BehaviorSubject, merge, Observable} from 'rxjs'; | |
import {map} from 'rxjs/operators'; | |
/** Flat node with expandable and level information */ | |
export class DynamicFlatNode<T> { | |
constructor(public item: T, public level: number = 1, public hasChildren: boolean = false, public isLoading: boolean = 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 abstract class DynamicDatabase<T> { | |
/** Initial data from database */ | |
initialData(): DynamicFlatNode<T>[] { | |
return this.getRootLevelItems().map(item => new DynamicFlatNode<T>(item, 0, true)); | |
} | |
abstract getRootLevelItems(): T[]; | |
abstract getChildren(item: T): Promise<T[] | undefined>; | |
abstract hasChildren(item: T): Promise<boolean>; | |
} | |
/** | |
* 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<T> { | |
dataChange: BehaviorSubject<DynamicFlatNode<T>[]> = new BehaviorSubject<DynamicFlatNode<T>[]>([]); | |
/** children cache */ | |
children = new Map<T, T[]>(); | |
get data(): DynamicFlatNode<T>[] { return this.dataChange.value; } | |
set data(value: DynamicFlatNode<T>[]) { | |
this.treeControl.dataNodes = value; | |
this.dataChange.next(value); | |
} | |
constructor(private treeControl: DynamicFlatTreeControl<T>, | |
private database: DynamicDatabase<T>) {} | |
connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode<T>[]> { | |
this.treeControl.expansionModel.changed.subscribe(change => { | |
if ((change as SelectionChange<DynamicFlatNode<T>>).added || | |
(change as SelectionChange<DynamicFlatNode<T>>).removed) { | |
this.handleTreeControl(change as SelectionChange<DynamicFlatNode<T>>); | |
} | |
}); | |
return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data)); | |
} | |
/** Handle expand/collapse behaviors */ | |
handleTreeControl(change: SelectionChange<DynamicFlatNode<T>>) { | |
if (change.added) { | |
change.added.forEach((node) => this.toggleNode(node, true)); | |
} | |
if (change.removed) { | |
change.removed.reverse().forEach((node) => this.toggleNode(node, false)); | |
} | |
} | |
/** | |
* Toggle the node, remove from display list | |
*/ | |
async toggleNode(node: DynamicFlatNode<T>, expand: boolean) { | |
let children: T[]; | |
if (this.children.has(node.item)) { | |
children = this.children.get(node.item); | |
} else { | |
node.isLoading = true; | |
children = await 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 | |
node.isLoading = false; | |
return; | |
} | |
if (expand) { | |
const nodesPromises: Promise<DynamicFlatNode<T>>[] = children.map(async item => | |
new DynamicFlatNode<T>(item, node.level + 1, await this.database.hasChildren(item))); | |
const nodes = await Promise.all(nodesPromises); | |
this.data.splice(index + 1, 0, ...nodes); | |
this.children.set(node.item, children); | |
} else { | |
const count = this.countInvisibleDescendants(node); | |
this.data.splice(index + 1, count); | |
this.children.delete(node.item); | |
} | |
// notify the change | |
this.dataChange.next(this.data); | |
node.isLoading = false; | |
} | |
countInvisibleDescendants(node: DynamicFlatNode<T>): number { | |
let count = 0; | |
if (!this.treeControl.isExpanded(node)) { | |
this.treeControl.getDescendants(node).map(child => { | |
count += 1 + this.countInvisibleDescendants(child); | |
}); | |
} | |
return count; | |
} | |
} | |
export class DynamicFlatTreeControl<T> extends FlatTreeControl<DynamicFlatNode<T>> { | |
constructor() { | |
super( | |
(node: DynamicFlatNode<T>) => node.level, // getLevel | |
(node: DynamicFlatNode<T>) => node.hasChildren); // isExpandable | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.example-tree-progress-bar { | |
margin-left: 30px; | |
} | |
.mat-tree-node { | |
padding: 0; | |
white-space: nowrap; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h1>https://material-tree-dynamic.stackblitz.io</h1> | |
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl"> | |
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding> | |
<button mat-icon-button disabled></button> | |
{{node.item}} | |
</mat-tree-node> | |
<mat-tree-node *matTreeNodeDef="let node; when: hasChildren" matTreeNodePadding> | |
<button mat-icon-button | |
[attr.aria-label]="'toggle ' + node.filename" matTreeNodeToggle> | |
<mat-icon> | |
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}} | |
</mat-icon> | |
</button> | |
{{node.item}} | |
<mat-progress-bar *ngIf="node.isLoading" | |
mode="indeterminate" | |
class="example-tree-progress-bar"></mat-progress-bar> | |
</mat-tree-node> | |
</mat-tree> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Component} from '@angular/core'; | |
import {DynamicDatabase, DynamicDataSource, DynamicFlatNode, DynamicFlatTreeControl} from '../common/dynamic-flat-tree'; | |
/** | |
* 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 GamesDatabase extends DynamicDatabase<string> { | |
dataMap = new Map<string, string[]>([ | |
['Simulation', ['Factorio', 'Oxygen not included']], | |
['Indie', [`Don't Starve`, 'Terraria', 'Starbound', 'Dungeon of the Endless']], | |
['Action', ['Overcooked']], | |
['Strategy', ['Rise to ruins']], | |
['RPG', ['Magicka']], | |
['Magicka', ['Magicka 1', 'Magicka 2']], | |
[`Don't Starve`, ['Region of Giants', 'Together', 'Shipwrecked']] | |
]); | |
rootLevelNodes = ['Simulation', 'Indie', 'Action', 'Strategy', 'RPG']; | |
getRootLevelItems(): string[] { | |
return this.rootLevelNodes; | |
} | |
getChildren(item: string): Promise<string[] | undefined> { | |
return new Promise(resolve => { | |
setTimeout(() => { | |
return resolve(this.dataMap.get(item)); | |
}, 1000); | |
}); | |
} | |
async hasChildren(item: string): Promise<boolean> { | |
return this.dataMap.has(item); | |
} | |
} | |
/** | |
* @title Tree with dynamic data | |
*/ | |
@Component({ | |
selector: 'app-test-flat', | |
templateUrl: 'flat.component.html', | |
styleUrls: ['flat.component.css'], | |
providers: [GamesDatabase] | |
}) | |
export class FlatComponent { | |
treeControl: DynamicFlatTreeControl<string>; | |
dataSource: DynamicDataSource<string>; | |
constructor(database: GamesDatabase) { | |
this.treeControl = new DynamicFlatTreeControl<string>(); | |
this.dataSource = new DynamicDataSource(this.treeControl, database); | |
this.dataSource.data = database.initialData(); | |
} | |
hasChildren = (_: number, nodeData: DynamicFlatNode<string>) => nodeData.hasChildren; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment