I want store the opening state of the nodes so that in virtual scrolling the user finds them already open if he had them open:
https://stackblitz.com/edit/stackblitz-starters-cqbnsy
import { bootstrapApplication } from '@angular/platform-browser';
import {
AfterViewInit,
Component,
ViewChild,
provideExperimentalZonelessChangeDetection,
} from '@angular/core';
import { Observable, of } from 'rxjs';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { FlatTreeControl } from '@angular/cdk/tree';
import {
MatTreeFlatDataSource,
MatTreeFlattener,
} from '@angular/material/tree';
import { MatIconModule } from '@angular/material/icon';
import { MatTreeModule } from '@angular/material/tree';
import { MatButtonModule } from '@angular/material/button';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export interface NestedFileSystemItem {
children: NestedFileSystemItem[];
name: string;
}
export interface NestedFileSystemItemFlatNode {
expandable: boolean;
name: string;
level: number;
}
const DATI = new Array(100_000)
.fill({
name: 'Folder',
children: [
{ name: 'File 1' },
{ name: 'File 2' },
{
name: 'Sub Folder',
children: [
{ name: 'Sub File 1' },
{ name: 'Sub File 2' },
{
name: 'Sub Sub Folder',
children: [{ name: 'Sub Sub File 1' }, { name: 'Sub Sub File 2' }],
},
],
},
],
})
.map((item, index) => {
return { ...item, name: item.name + ' (' + index + ')' };
});
@Component({
selector: 'app-root',
standalone: true,
imports: [MatIconModule, MatButtonModule, MatTreeModule, ScrollingModule],
styles: `@import "https://fonts.googleapis.com/icon?family=Material+Icons";`,
template: `
<cdk-virtual-scroll-viewport itemSize="48" style="height: 500px;">
<ng-container *cdkVirtualFor="let item of fullDataSource"></ng-container>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
<button mat-icon-button disabled></button>
{{node.name}}
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
</mat-tree-node>
</mat-tree>
</cdk-virtual-scroll-viewport>
`,
})
export class AppComponent implements AfterViewInit {
treeControl: FlatTreeControl<NestedFileSystemItemFlatNode>;
treeFlattener: MatTreeFlattener<
NestedFileSystemItem,
NestedFileSystemItemFlatNode
>;
dataSource: MatTreeFlatDataSource<
NestedFileSystemItem,
NestedFileSystemItemFlatNode
>;
fullDataSource = DATI;
@ViewChild(CdkVirtualScrollViewport) virtualScroll!: CdkVirtualScrollViewport;
constructor() {
this.treeFlattener = new MatTreeFlattener(
this.transformer,
this._getLevel,
this._isExpandable,
this._getChildren
);
this.treeControl = new FlatTreeControl<NestedFileSystemItemFlatNode>(
this._getLevel,
this._isExpandable
);
this.dataSource = new MatTreeFlatDataSource(
this.treeControl,
this.treeFlattener
);
this.dataSource.data = this.fullDataSource.slice(0, 10);
}
transformer = (
node: NestedFileSystemItem,
level: number
): NestedFileSystemItemFlatNode => ({
expandable: !!node.children,
name: node.name,
level,
});
_getLevel = (node: NestedFileSystemItemFlatNode) => node.level;
_isExpandable = (node: NestedFileSystemItemFlatNode) => node.expandable;
_getChildren = (
node: NestedFileSystemItem
): Observable<NestedFileSystemItem[]> => of(node.children);
hasChild = (_: number, _nodeData: NestedFileSystemItemFlatNode) =>
_nodeData.expandable;
ngAfterViewInit() {
this.virtualScroll.renderedRangeStream.subscribe((range: any) => {
this.dataSource.data = this.fullDataSource.slice(range.start, range.end);
});
}
}
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
],
}).catch((err) => console.error(err));
Unfortunately I am unable to make it work with flat datasource
but it is achievable using nested data source
, please find below working stackblitz and a reference stackblitz used for solving this problem. This nested tree data has the advantage of controlling the child rendering container, which we can hide using the css [class.example-tree-invisible]="node.expanded"
and by using the property expanded
which we store on the data object, we can make the view remember which was opened.
import { bootstrapApplication } from '@angular/platform-browser';
import {
AfterViewInit,
Component,
ViewChild,
provideExperimentalZonelessChangeDetection,
} from '@angular/core';
import { Observable, of } from 'rxjs';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { FlatTreeControl, NestedTreeControl } from '@angular/cdk/tree';
import {
MatTreeFlatDataSource,
MatTreeFlattener,
MatTreeNestedDataSource,
} from '@angular/material/tree';
import { MatIconModule } from '@angular/material/icon';
import { MatTreeModule } from '@angular/material/tree';
import { MatButtonModule } from '@angular/material/button';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { CommonModule } from '@angular/common';
export interface NestedFileSystemItem {
children: NestedFileSystemItem[];
name: string;
}
export interface NestedFileSystemItemFlatNode {
expandable: boolean;
name: string;
level: number;
}
const DATI = new Array(100_000)
.fill({
name: 'Folder',
children: [
{ name: 'File 1' },
{ name: 'File 2' },
{
name: 'Sub Folder',
children: [
{ name: 'Sub File 1' },
{ name: 'Sub File 2' },
{
name: 'Sub Sub Folder',
children: [{ name: 'Sub Sub File 1' }, { name: 'Sub Sub File 2' }],
},
],
},
],
})
.map((item, index) => {
return { ...item, name: item.name + ' (' + index + ')' };
});
@Component({
selector: 'app-root',
standalone: true,
imports: [
MatIconModule,
MatButtonModule,
MatTreeModule,
ScrollingModule,
CommonModule,
],
styles: `@import "https://fonts.googleapis.com/icon?family=Material+Icons";`,
template: `
<cdk-virtual-scroll-viewport itemSize="48" style="height: 500px;">
<ng-container *cdkVirtualFor="let item of fullDataSource"></ng-container>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="example-tree">
<mat-tree-node *matTreeNodeDef="let node">
<li class="mat-tree-node">
<button mat-icon-button disabled></button>
{{node.name}}
</li>
</mat-tree-node>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasNestedChild">
<li>
<div class="mat-tree-node">
<button mat-icon-button
[attr.aria-label]="'toggle ' + node.name"
(click)="changeState(node)">
<mat-icon class="mat-icon-rtl-mirror">
{{node.expanded ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
</div>
<ul [class.example-tree-invisible]="node.expanded">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</li>
</mat-nested-tree-node>
</mat-tree>
</cdk-virtual-scroll-viewport>
`,
})
export class AppComponent implements AfterViewInit {
treeControl: FlatTreeControl<NestedFileSystemItemFlatNode>;
treeFlattener: any;
dataSource: any;
fullDataSource = DATI;
@ViewChild(CdkVirtualScrollViewport) virtualScroll!: CdkVirtualScrollViewport;
constructor() {
this.treeControl = new NestedTreeControl<any>(this._getChildren);
this.dataSource = new MatTreeNestedDataSource();
this.dataSource.data = this.fullDataSource.slice(0, 10);
}
transformer = (
node: NestedFileSystemItem,
level: number
): NestedFileSystemItemFlatNode => ({
expandable: !!node.children,
name: node.name,
level,
});
private _getChildren = (node: any) => node?.children;
ngAfterViewInit() {
this.virtualScroll.renderedRangeStream.subscribe((range: any) => {
this.dataSource.data = this.fullDataSource.slice(range.start, range.end);
});
}
hasNestedChild = (_: number, nodeData: any) => nodeData?.children?.length > 0;
changeState(node: any) {
node.expanded = !node.expanded;
console.log(node);
}
}
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
],
}).catch((err) => console.error(err));