1- import { CollectionViewer , SelectionChange , DataSource } from '@angular/cdk/collections' ;
2- import { FlatTreeControl } from '@angular/cdk/tree' ;
31import { ChangeDetectionStrategy , Component , Injectable , inject , signal } from '@angular/core' ;
4- import { BehaviorSubject , merge , Observable } from 'rxjs' ;
5- import { map } from 'rxjs/operators' ;
62import { MatProgressBarModule } from '@angular/material/progress-bar' ;
73import { MatIconModule } from '@angular/material/icon' ;
84import { MatButtonModule } from '@angular/material/button' ;
95import { MatTreeModule } from '@angular/material/tree' ;
106
11- /** Flat node with expandable and level information */
12- class DynamicFlatNode {
13- constructor (
14- public item : string ,
15- public level = 1 ,
16- public expandable = false ,
17- public isLoading = signal ( false ) ,
18- ) { }
7+ /** Node with expandable and level information */
8+ interface DynamicNode {
9+ name : string ;
10+ level : number ;
11+ expandable : boolean ;
12+ isLoading : ReturnType < typeof signal < boolean > > ;
13+ children ?: DynamicNode [ ] ;
1914}
2015
2116/**
@@ -34,102 +29,26 @@ export class DynamicDatabase {
3429 rootLevelNodes : string [ ] = [ 'Fruits' , 'Vegetables' ] ;
3530
3631 /** Initial data from database */
37- initialData ( ) : DynamicFlatNode [ ] {
38- return this . rootLevelNodes . map ( name => new DynamicFlatNode ( name , 0 , true ) ) ;
32+ initialData ( ) : DynamicNode [ ] {
33+ return this . rootLevelNodes . map ( name => this . createNode ( name , 0 , true ) ) ;
3934 }
4035
41- getChildren ( node : string ) : string [ ] | undefined {
42- return this . dataMap . get ( node ) ;
36+ createNode ( name : string , level : number , expandable : boolean ) : DynamicNode {
37+ return {
38+ name,
39+ level,
40+ expandable,
41+ isLoading : signal ( false ) ,
42+ children : undefined ,
43+ } ;
4344 }
4445
45- isExpandable ( node : string ) : boolean {
46- return this . dataMap . has ( node ) ;
46+ getChildren ( name : string ) : string [ ] | undefined {
47+ return this . dataMap . get ( name ) ;
4748 }
48- }
49- /**
50- * File database, it can build a tree structured Json object from string.
51- * Each node in Json object represents a file or a directory. For a file, it has filename and type.
52- * For a directory, it has filename and children (a list of files or directories).
53- * The input will be a json object string, and the output is a list of `FileNode` with nested
54- * structure.
55- */
56- export class DynamicDataSource implements DataSource < DynamicFlatNode > {
57- dataChange = new BehaviorSubject < DynamicFlatNode [ ] > ( [ ] ) ;
58-
59- get data ( ) : DynamicFlatNode [ ] {
60- return this . dataChange . value ;
61- }
62- set data ( value : DynamicFlatNode [ ] ) {
63- this . _treeControl . dataNodes = value ;
64- this . dataChange . next ( value ) ;
65- }
66-
67- constructor (
68- private _treeControl : FlatTreeControl < DynamicFlatNode > ,
69- private _database : DynamicDatabase ,
70- ) { }
71-
72- connect ( collectionViewer : CollectionViewer ) : Observable < DynamicFlatNode [ ] > {
73- this . _treeControl . expansionModel . changed . subscribe ( change => {
74- if (
75- ( change as SelectionChange < DynamicFlatNode > ) . added ||
76- ( change as SelectionChange < DynamicFlatNode > ) . removed
77- ) {
78- this . handleTreeControl ( change as SelectionChange < DynamicFlatNode > ) ;
79- }
80- } ) ;
81-
82- return merge ( collectionViewer . viewChange , this . dataChange ) . pipe ( map ( ( ) => this . data ) ) ;
83- }
84-
85- disconnect ( collectionViewer : CollectionViewer ) : void { }
86-
87- /** Handle expand/collapse behaviors */
88- handleTreeControl ( change : SelectionChange < DynamicFlatNode > ) {
89- if ( change . added ) {
90- change . added . forEach ( node => this . toggleNode ( node , true ) ) ;
91- }
92- if ( change . removed ) {
93- change . removed
94- . slice ( )
95- . reverse ( )
96- . forEach ( node => this . toggleNode ( node , false ) ) ;
97- }
98- }
99-
100- /**
101- * Toggle the node, remove from display list
102- */
103- toggleNode ( node : DynamicFlatNode , expand : boolean ) {
104- const children = this . _database . getChildren ( node . item ) ;
105- const index = this . data . indexOf ( node ) ;
106- if ( ! children || index < 0 ) {
107- // If no children, or cannot find the node, no op
108- return ;
109- }
110-
111- node . isLoading . set ( true ) ;
112-
113- setTimeout ( ( ) => {
114- if ( expand ) {
115- const nodes = children . map (
116- name => new DynamicFlatNode ( name , node . level + 1 , this . _database . isExpandable ( name ) ) ,
117- ) ;
118- this . data . splice ( index + 1 , 0 , ...nodes ) ;
119- } else {
120- let count = 0 ;
121- for (
122- let i = index + 1 ;
123- i < this . data . length && this . data [ i ] . level > node . level ;
124- i ++ , count ++
125- ) { }
126- this . data . splice ( index + 1 , count ) ;
127- }
12849
129- // notify the change
130- this . dataChange . next ( this . data ) ;
131- node . isLoading . set ( false ) ;
132- } , 1000 ) ;
50+ isExpandable ( name : string ) : boolean {
51+ return this . dataMap . has ( name ) ;
13352 }
13453}
13554
@@ -144,22 +63,37 @@ export class DynamicDataSource implements DataSource<DynamicFlatNode> {
14463 changeDetection : ChangeDetectionStrategy . OnPush ,
14564} )
14665export class TreeDynamicExample {
147- constructor ( ) {
148- const database = inject ( DynamicDatabase ) ;
66+ private _database = inject ( DynamicDatabase ) ;
14967
150- this . treeControl = new FlatTreeControl < DynamicFlatNode > ( this . getLevel , this . isExpandable ) ;
151- this . dataSource = new DynamicDataSource ( this . treeControl , database ) ;
68+ dataSource = this . _database . initialData ( ) ;
15269
153- this . dataSource . data = database . initialData ( ) ;
154- }
70+ childrenAccessor = ( node : DynamicNode ) => node . children ?? [ ] ;
15571
156- treeControl : FlatTreeControl < DynamicFlatNode > ;
72+ hasChild = ( _ : number , node : DynamicNode ) => node . expandable ;
15773
158- dataSource : DynamicDataSource ;
74+ /**
75+ * Load children on node expansion.
76+ * Called from template via (expandedChange) output.
77+ */
78+ onNodeExpanded ( node : DynamicNode , expanded : boolean ) : void {
79+ if ( ! expanded || node . children ) {
80+ // Don't reload if collapsing or already loaded
81+ return ;
82+ }
15983
160- getLevel = ( node : DynamicFlatNode ) => node . level ;
84+ const childNames = this . _database . getChildren ( node . name ) ;
85+ if ( ! childNames ) {
86+ return ;
87+ }
16188
162- isExpandable = ( node : DynamicFlatNode ) => node . expandable ;
89+ node . isLoading . set ( true ) ;
16390
164- hasChild = ( _ : number , _nodeData : DynamicFlatNode ) => _nodeData . expandable ;
91+ // Simulate async data loading
92+ setTimeout ( ( ) => {
93+ node . children = childNames . map ( name =>
94+ this . _database . createNode ( name , node . level + 1 , this . _database . isExpandable ( name ) ) ,
95+ ) ;
96+ node . isLoading . set ( false ) ;
97+ } , 1000 ) ;
98+ }
16599}
0 commit comments