/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {CollectionViewer, DataSource} from '@angular/cdk/collections'; import {FlatTreeControl, TreeControl} from '@angular/cdk/tree'; import {BehaviorSubject, merge, Observable} from 'rxjs'; import {map, take} from 'rxjs/operators'; /** * Tree flattener to convert a normal type of node to node with children & level information. * Transform nested nodes of type `T` to flattened nodes of type `F`. * * For example, the input data of type `T` is nested, and contains its children data: * SomeNode: { * key: 'Fruits', * children: [ * NodeOne: { * key: 'Apple', * }, * NodeTwo: { * key: 'Pear', * } * ] * } * After flattener flatten the tree, the structure will become * SomeNode: { * key: 'Fruits', * expandable: true, * level: 1 * }, * NodeOne: { * key: 'Apple', * expandable: false, * level: 2 * }, * NodeTwo: { * key: 'Pear', * expandable: false, * level: 2 * } * and the output flattened type is `F` with additional information. */ export class MatTreeFlattener { constructor(public transformFunction: (node: T, level: number) => F, public getLevel: (node: F) => number, public isExpandable: (node: F) => boolean, public getChildren: (node: T) => Observable | T[] | undefined | null) {} _flattenNode(node: T, level: number, resultNodes: F[], parentMap: boolean[]): F[] { const flatNode = this.transformFunction(node, level); resultNodes.push(flatNode); if (this.isExpandable(flatNode)) { const childrenNodes = this.getChildren(node); if (childrenNodes) { if (Array.isArray(childrenNodes)) { this._flattenChildren(childrenNodes, level, resultNodes, parentMap); } else { childrenNodes.pipe(take(1)).subscribe(children => { this._flattenChildren(children, level, resultNodes, parentMap); }); } } } return resultNodes; } _flattenChildren(children: T[], level: number, resultNodes: F[], parentMap: boolean[]): void { children.forEach((child, index) => { let childParentMap: boolean[] = parentMap.slice(); childParentMap.push(index != children.length - 1); this._flattenNode(child, level + 1, resultNodes, childParentMap); }); } /** * Flatten a list of node type T to flattened version of node F. * Please note that type T may be nested, and the length of `structuredData` may be different * from that of returned list `F[]`. */ flattenNodes(structuredData: T[]): F[] { let resultNodes: F[] = []; structuredData.forEach(node => this._flattenNode(node, 0, resultNodes, [])); return resultNodes; } /** * Expand flattened node with current expansion status. * The returned list may have different length. */ expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[] { let results: F[] = []; let currentExpand: boolean[] = []; currentExpand[0] = true; nodes.forEach(node => { let expand = true; for (let i = 0; i <= this.getLevel(node); i++) { expand = expand && currentExpand[i]; } if (expand) { results.push(node); } if (this.isExpandable(node)) { currentExpand[this.getLevel(node) + 1] = treeControl.isExpanded(node); } }); return results; } } /** * Data source for flat tree. * The data source need to handle expansion/collapsion of the tree node and change the data feed * to `MatTree`. * The nested tree nodes of type `T` are flattened through `MatTreeFlattener`, and converted * to type `F` for `MatTree` to consume. */ export class MatTreeFlatDataSource extends DataSource { _flattenedData = new BehaviorSubject([]); _expandedData = new BehaviorSubject([]); _data: BehaviorSubject; get data() { return this._data.value; } set data(value: T[]) { this._data.next(value); this._flattenedData.next(this._treeFlattener.flattenNodes(this.data)); this._treeControl.dataNodes = this._flattenedData.value; } constructor(private _treeControl: FlatTreeControl, private _treeFlattener: MatTreeFlattener, initialData: T[] = []) { super(); this._data = new BehaviorSubject(initialData); } connect(collectionViewer: CollectionViewer): Observable { const changes = [ collectionViewer.viewChange, this._treeControl.expansionModel.onChange, this._flattenedData ]; return merge(...changes).pipe(map(() => { this._expandedData.next( this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl)); return this._expandedData.value; })); } disconnect() { // no op } }