diff --git a/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.html b/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.html index c1ed7696fe5f..6ece80fd7c3f 100644 --- a/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.html +++ b/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.html @@ -1,21 +1,18 @@ - + - {{node.item}} + {{node.name}} - - {{node.item}} + {{node.name}} @if (node.isLoading()) { - + } - + \ No newline at end of file diff --git a/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.ts b/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.ts index 84698afd315e..b01c0f154cd6 100644 --- a/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.ts +++ b/src/components-examples/material/tree/tree-dynamic/tree-dynamic-example.ts @@ -1,21 +1,16 @@ -import {CollectionViewer, SelectionChange, DataSource} from '@angular/cdk/collections'; -import {FlatTreeControl} from '@angular/cdk/tree'; import {ChangeDetectionStrategy, Component, Injectable, inject, signal} from '@angular/core'; -import {BehaviorSubject, merge, Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; import {MatProgressBarModule} from '@angular/material/progress-bar'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; import {MatTreeModule} from '@angular/material/tree'; -/** Flat node with expandable and level information */ -class DynamicFlatNode { - constructor( - public item: string, - public level = 1, - public expandable = false, - public isLoading = signal(false), - ) {} +/** Node with expandable and level information */ +interface DynamicNode { + name: string; + level: number; + expandable: boolean; + isLoading: ReturnType>; + children?: DynamicNode[]; } /** @@ -34,102 +29,26 @@ export class DynamicDatabase { rootLevelNodes: string[] = ['Fruits', 'Vegetables']; /** Initial data from database */ - initialData(): DynamicFlatNode[] { - return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 0, true)); + initialData(): DynamicNode[] { + return this.rootLevelNodes.map(name => this.createNode(name, 0, true)); } - getChildren(node: string): string[] | undefined { - return this.dataMap.get(node); + createNode(name: string, level: number, expandable: boolean): DynamicNode { + return { + name, + level, + expandable, + isLoading: signal(false), + children: undefined, + }; } - isExpandable(node: string): boolean { - return this.dataMap.has(node); + getChildren(name: string): string[] | undefined { + return this.dataMap.get(name); } -} -/** - * 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. - */ -export class DynamicDataSource implements DataSource { - 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.changed.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)); - } - - disconnect(collectionViewer: CollectionViewer): void {} - - /** 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.set(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.set(false); - }, 1000); + isExpandable(name: string): boolean { + return this.dataMap.has(name); } } @@ -144,22 +63,37 @@ export class DynamicDataSource implements DataSource { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TreeDynamicExample { - constructor() { - const database = inject(DynamicDatabase); + private _database = inject(DynamicDatabase); - this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - this.dataSource = new DynamicDataSource(this.treeControl, database); + dataSource = this._database.initialData(); - this.dataSource.data = database.initialData(); - } + childrenAccessor = (node: DynamicNode) => node.children ?? []; - treeControl: FlatTreeControl; + hasChild = (_: number, node: DynamicNode) => node.expandable; - dataSource: DynamicDataSource; + /** + * Load children on node expansion. + * Called from template via (expandedChange) output. + */ + onNodeExpanded(node: DynamicNode, expanded: boolean): void { + if (!expanded || node.children) { + // Don't reload if collapsing or already loaded + return; + } - getLevel = (node: DynamicFlatNode) => node.level; + const childNames = this._database.getChildren(node.name); + if (!childNames) { + return; + } - isExpandable = (node: DynamicFlatNode) => node.expandable; + node.isLoading.set(true); - hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable; + // Simulate async data loading + setTimeout(() => { + node.children = childNames.map(name => + this._database.createNode(name, node.level + 1, this._database.isExpandable(name)), + ); + node.isLoading.set(false); + }, 1000); + } } diff --git a/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.html b/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.html index e70aba031ae6..3d4da1ac46ad 100644 --- a/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.html +++ b/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.html @@ -1,4 +1,4 @@ - + @@ -11,7 +11,7 @@ {{node.name}} diff --git a/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.ts b/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.ts index 4211f713c539..b8f7d2be14e9 100644 --- a/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.ts +++ b/src/components-examples/material/tree/tree-flat-overview/tree-flat-overview-example.ts @@ -1,6 +1,5 @@ -import {FlatTreeControl} from '@angular/cdk/tree'; import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree'; +import {MatTreeModule} from '@angular/material/tree'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; @@ -13,13 +12,6 @@ interface FoodNode { children?: FoodNode[]; } -/** Flat node with expandable and level information */ -interface ExampleFlatNode { - expandable: boolean; - name: string; - level: number; -} - /** * @title Tree with flat nodes */ @@ -30,33 +22,11 @@ interface ExampleFlatNode { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TreeFlatOverviewExample { - private _transformer = (node: FoodNode, level: number) => { - return { - expandable: !!node.children && node.children.length > 0, - name: node.name, - level: level, - }; - }; - - treeControl = new FlatTreeControl( - node => node.level, - node => node.expandable, - ); - - treeFlattener = new MatTreeFlattener( - this._transformer, - node => node.level, - node => node.expandable, - node => node.children, - ); - - dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + dataSource = EXAMPLE_DATA; - constructor() { - this.dataSource.data = EXAMPLE_DATA; - } + childrenAccessor = (node: FoodNode) => node.children ?? []; - hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + hasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0; } const EXAMPLE_DATA: FoodNode[] = [ diff --git a/src/components-examples/material/tree/tree-harness/tree-harness-example.html b/src/components-examples/material/tree/tree-harness/tree-harness-example.html index e70aba031ae6..53509fdba1f9 100644 --- a/src/components-examples/material/tree/tree-harness/tree-harness-example.html +++ b/src/components-examples/material/tree/tree-harness/tree-harness-example.html @@ -1,4 +1,4 @@ - + @@ -7,13 +7,12 @@ - {{node.name}} - + \ No newline at end of file diff --git a/src/components-examples/material/tree/tree-harness/tree-harness-example.ts b/src/components-examples/material/tree/tree-harness/tree-harness-example.ts index 8f3b4b4cd40f..9d199707763a 100644 --- a/src/components-examples/material/tree/tree-harness/tree-harness-example.ts +++ b/src/components-examples/material/tree/tree-harness/tree-harness-example.ts @@ -1,6 +1,5 @@ -import {FlatTreeControl} from '@angular/cdk/tree'; import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree'; +import {MatTreeModule} from '@angular/material/tree'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; @@ -9,12 +8,6 @@ interface Node { children?: Node[]; } -interface ExampleFlatNode { - expandable: boolean; - name: string; - level: number; -} - /** * @title Testing with MatTreeHarness */ @@ -25,33 +18,11 @@ interface ExampleFlatNode { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TreeHarnessExample { - private _transformer = (node: Node, level: number) => { - return { - expandable: !!node.children && node.children.length > 0, - name: node.name, - level: level, - }; - }; - - treeControl = new FlatTreeControl( - node => node.level, - node => node.expandable, - ); - - treeFlattener = new MatTreeFlattener( - this._transformer, - node => node.level, - node => node.expandable, - node => node.children, - ); - - dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + dataSource = EXAMPLE_DATA; - constructor() { - this.dataSource.data = EXAMPLE_DATA; - } + childrenAccessor = (node: Node) => node.children ?? []; - hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + hasChild = (_: number, node: Node) => !!node.children && node.children.length > 0; } const EXAMPLE_DATA: Node[] = [ diff --git a/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.html b/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.html index 88bf949e1737..14c41e3f0c64 100644 --- a/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.html +++ b/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.html @@ -1,4 +1,4 @@ - + @@ -7,20 +7,17 @@ - {{node.name}} - + Load more of {{node.parent}}... - + \ No newline at end of file diff --git a/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.ts b/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.ts index da8da8b5b15a..d3d7302c5d62 100644 --- a/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.ts +++ b/src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.ts @@ -5,56 +5,32 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import {FlatTreeControl} from '@angular/cdk/tree'; -import {ChangeDetectionStrategy, Component, Injectable, inject} from '@angular/core'; -import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree'; -import {BehaviorSubject, Observable} from 'rxjs'; +import {ChangeDetectionStrategy, Component, Injectable, inject, signal} from '@angular/core'; +import {MatTreeModule} from '@angular/material/tree'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; import {ENTER, SPACE} from '@angular/cdk/keycodes'; -let loadMoreId = 1; - -/** Nested node */ -class NestedNode { - childrenChange = new BehaviorSubject([]); - - get children(): NestedNode[] { - return this.childrenChange.value; - } - - constructor( - public name: string, - public hasChildren = false, - public parent: string | null = null, - public isLoadMore = false, - ) {} -} - -/** Flat node with expandable and level information */ -export class FlatNode { - constructor( - public name: string, - public level = 1, - public expandable = false, - public parent: string | null = null, - public isLoadMore = false, - ) {} +/** Node data with optional children */ +interface TreeNode { + name: string; + parent: string | null; + expandable: boolean; + isLoadMore: boolean; + children?: TreeNode[]; } /** Number of nodes loaded at a time */ const batchSize = 3; /** - * A database that only load part of the data initially. After user clicks on the `Load more` + * A database that only loads part of the data initially. After user clicks on the `Load more` * button, more data will be loaded. */ @Injectable() export class LoadmoreDatabase { /** Map of node name to node */ - nodes = new Map(); - - dataChange = new BehaviorSubject([]); + private _nodes = new Map(); /** Example data */ rootNodes: string[] = ['Vegetables', 'Fruits']; @@ -80,42 +56,53 @@ export class LoadmoreDatabase { ['Onion', ['Yellow', 'White', 'Purple', 'Green', 'Shallot', 'Sweet', 'Red', 'Leek']], ]); - initialize() { - const data = this.rootNodes.map(name => this._generateNode(name, null)); - this.dataChange.next(data); + initialize(): TreeNode[] { + return this.rootNodes.map(name => this._getOrCreateNode(name, null)); } - /** Expand a node whose children are not loaded */ - loadChildren(name: string, onlyFirstTime = false) { - if (!this.nodes.has(name) || !this.childMap.has(name)) { - return; + private _getOrCreateNode(name: string, parent: string | null): TreeNode { + if (!this._nodes.has(name)) { + this._nodes.set(name, { + name, + parent, + expandable: this.childMap.has(name), + isLoadMore: false, + children: undefined, + }); } - const parent = this.nodes.get(name)!; - const children = this.childMap.get(name)!; + return this._nodes.get(name)!; + } - if (onlyFirstTime && parent.children!.length > 0) { + /** Load children for a node, with pagination support */ + loadChildren(parentName: string, onlyFirstTime = false): void { + const parent = this._nodes.get(parentName); + const childNames = this.childMap.get(parentName); + if (!parent || !childNames) { return; } - const newChildrenNumber = parent.children!.length + batchSize; - const nodes = children - .slice(0, newChildrenNumber) - .map(name => this._generateNode(name, parent.name)); - if (newChildrenNumber < children.length) { - // Need a new "Load More" node - nodes.push(new NestedNode(`LOAD_MORE-${loadMoreId++}`, false, name, true)); + if (onlyFirstTime && parent.children && parent.children.length > 0) { + return; } - parent.childrenChange.next(nodes); - this.dataChange.next(this.dataChange.value); - } - - private _generateNode(name: string, parent: string | null): NestedNode { - if (!this.nodes.has(name)) { - this.nodes.set(name, new NestedNode(name, this.childMap.has(name), parent)); + const currentChildCount = parent.children?.filter(c => !c.isLoadMore).length ?? 0; + const newChildCount = currentChildCount + batchSize; + + const children = childNames + .slice(0, newChildCount) + .map(name => this._getOrCreateNode(name, parentName)); + + // Add "Load more" node if there are more children + if (newChildCount < childNames.length) { + children.push({ + name: `LOAD_MORE_${parentName}_${Date.now()}`, + parent: parentName, + expandable: false, + isLoadMore: true, + }); } - return this.nodes.get(name)!; + parent.children = children; } } @@ -133,73 +120,37 @@ export class LoadmoreDatabase { export class TreeLoadmoreExample { private _database = inject(LoadmoreDatabase); - nodeMap = new Map(); - treeControl: FlatTreeControl; - treeFlattener: MatTreeFlattener; - // Flat tree data source - dataSource: MatTreeFlatDataSource; - - constructor() { - const _database = this._database; - - this.treeFlattener = new MatTreeFlattener( - this.transformer, - this.getLevel, - this.isExpandable, - this.getChildren, - ); + dataSource = signal([]); - // TODO(#27626): Remove treeControl. Adopt either levelAccessor or childrenAccessor. - this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + childrenAccessor = (node: TreeNode) => node.children ?? []; - this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + hasChild = (_: number, node: TreeNode) => node.expandable; - _database.dataChange.subscribe(data => { - this.dataSource.data = data; - }); + isLoadMore = (_: number, node: TreeNode) => node.isLoadMore; - _database.initialize(); + constructor() { + this.dataSource.set(this._database.initialize()); } - getChildren = (node: NestedNode): Observable => node.childrenChange; - - transformer = (node: NestedNode, level: number) => { - const existingNode = this.nodeMap.get(node.name); - - if (existingNode) { - return existingNode; - } - - const newNode = new FlatNode(node.name, level, node.hasChildren, node.parent, node.isLoadMore); - this.nodeMap.set(node.name, newNode); - return newNode; - }; - - getLevel = (node: FlatNode) => node.level; - - isExpandable = (node: FlatNode) => node.expandable; - - hasChild = (_: number, node: FlatNode) => node.expandable; - - isLoadMore = (_: number, node: FlatNode) => node.isLoadMore; - - loadChildren(node: FlatNode) { + loadChildren(node: TreeNode) { this._database.loadChildren(node.name, true); + // Trigger change detection by updating the signal + this.dataSource.set([...this.dataSource()]); } /** Load more nodes when clicking on "Load more" node. */ - loadOnClick(event: MouseEvent, node: FlatNode) { + loadOnClick(event: MouseEvent, node: TreeNode) { this._loadSiblings(event.target as HTMLElement, node); } - /** Load more nodes on keyboardpress when focused on "Load more" node */ - loadOnKeypress(event: KeyboardEvent, node: FlatNode) { + /** Load more nodes on keypress when focused on "Load more" node */ + loadOnKeypress(event: KeyboardEvent, node: TreeNode) { if (event.keyCode === ENTER || event.keyCode === SPACE) { this._loadSiblings(event.target as HTMLElement, node); } } - private _loadSiblings(nodeElement: HTMLElement, node: FlatNode) { + private _loadSiblings(nodeElement: HTMLElement, node: TreeNode) { if (node.parent) { // Store a reference to the sibling of the "Load More" node before it is removed from the DOM const previousSibling = nodeElement.previousElementSibling; @@ -207,11 +158,14 @@ export class TreeLoadmoreExample { // Synchronously load data. this._database.loadChildren(node.parent); - const focusDesination = previousSibling?.nextElementSibling || previousSibling; + // Trigger change detection + this.dataSource.set([...this.dataSource()]); + + const focusDestination = previousSibling?.nextElementSibling || previousSibling; - if (focusDesination) { + if (focusDestination) { // Restore focus. - (focusDesination as HTMLElement).focus(); + (focusDestination as HTMLElement).focus(); } } } diff --git a/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.html b/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.html index bd6eed8f9fa8..ccc3c33c05ed 100644 --- a/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.html +++ b/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.html @@ -1,4 +1,4 @@ - + @@ -6,23 +6,20 @@ {{node.name}} - +
- {{node.name}}
-
+
- + \ No newline at end of file diff --git a/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.ts b/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.ts index 3500b3a3d034..70bcabc15213 100644 --- a/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.ts +++ b/src/components-examples/material/tree/tree-nested-overview/tree-nested-overview-example.ts @@ -1,6 +1,5 @@ -import {NestedTreeControl} from '@angular/cdk/tree'; import {ChangeDetectionStrategy, Component} from '@angular/core'; -import {MatTreeNestedDataSource, MatTreeModule} from '@angular/material/tree'; +import {MatTreeModule} from '@angular/material/tree'; import {MatIconModule} from '@angular/material/icon'; import {MatButtonModule} from '@angular/material/button'; @@ -24,12 +23,9 @@ interface FoodNode { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TreeNestedOverviewExample { - treeControl = new NestedTreeControl(node => node.children); - dataSource = new MatTreeNestedDataSource(); + childrenAccessor = (node: FoodNode) => node.children ?? []; - constructor() { - this.dataSource.data = EXAMPLE_DATA; - } + dataSource = EXAMPLE_DATA; hasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0; }