如何将现有ng-container递归树适配为cdk虚拟滚动树?
解决递归树形结构结合CDK虚拟滚动的问题
这问题我之前也碰到过!核心问题是递归模板里的cdkVirtualFor找不到CdkVirtualScrollViewport的实例——因为默认情况下cdkVirtualFor会从当前组件上下文注入viewport,但递归调用的模板上下文里没有这个引用,所以才会报No provider for CdkVirtualScrollViewport的错误。另外展开/折叠的逻辑也要适配虚拟滚动的特性,下面给你两种可行的解决方案:
方案一:保留递归DOM结构,传递Viewport引用
这种方案能保留你原有的树形DOM结构,只需要把viewport引用通过模板上下文传递给递归模板即可:
步骤1:确保导入ScrollerModule
首先确认你的NgModule里已经导入了ScrollerModule(这是基础,很多人容易漏):
import { ScrollerModule } from '@angular/cdk/scrolling'; @NgModule({ imports: [ // ...其他模块 ScrollerModule ] }) export class YourModule { }
步骤2:组件中获取Viewport引用
在组件类里通过@ViewChild获取虚拟滚动容器的引用:
import { Component, ViewChild } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; @Component({ // ...组件元数据 }) export class YourTreeComponent { @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport; root = [{ id: "aec04ef4-fc6a-481f-a12d-0ce987d494b3", expanded: true, children: [{ id: "cbf9e862-e932-410f-ad84-ed0a5ec1917a", expanded: false, children: [] }] }]; trackById(index: number, item: any) { return item.id; } }
步骤3:修改递归模板传递Viewport
把viewport引用通过模板上下文传递给递归的ngTemplateOutlet,并在cdkVirtualFor中明确指定使用该viewport:
<cdk-virtual-scroll-viewport style="height: 750px" itemSize="50" #viewport> <ng-template #recursiveList let-root let-viewport="viewport"> <li *cdkVirtualFor="let item of root; trackBy: trackById; viewport: viewport"> <!-- 展开/折叠按钮 --> <button (click)="item.expanded = !item.expanded"> {{ item.expanded ? '-' : '+' }} {{ item.id }} </button> <!-- 递归渲染子节点,传递viewport引用 --> <ul [class.hide]="!item.expanded" *ngIf="item.children?.length > 0"> <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: item.children, viewport: viewport }"></ng-container> </ul> </li> </ng-template> <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: root, viewport: viewport }"></ng-container> </cdk-virtual-scroll-viewport>
补充CSS
.hide { display: none; }
这种方案的优势是完全保留你原有的DOM结构,不需要修改数据格式,适合需要复杂子节点布局的场景。
方案二:扁平化树形数据,单虚拟滚动容器(性能更优)
如果你的树形结构有数千个节点,这种方案的性能会更好——因为只需要一个虚拟滚动容器,避免了递归嵌套带来的额外开销。核心思路是把树形数据扁平化,通过CSS缩进模拟层级,展开/折叠时重新生成扁平化数组:
步骤1:组件中实现数据扁平化和展开逻辑
import { Component } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; @Component({ // ...组件元数据 }) export class YourTreeComponent { root = [{ id: "aec04ef4-fc6a-481f-a12d-0ce987d494b3", expanded: true, children: [{ id: "cbf9e862-e932-410f-ad84-ed0a5ec1917a", expanded: false, children: [] }] }]; flattenedTree = this.flattenTree(this.root); trackById(index: number, item: any) { return item.id; } // 递归扁平化树形数据,添加level字段表示层级 flattenTree(nodes: any[], level = 0): any[] { return nodes.reduce((acc, node) => { acc.push({ ...node, level }); // 只添加展开状态的子节点 if (node.expanded && node.children?.length) { acc.push(...this.flattenTree(node.children, level + 1)); } return acc; }, []); } // 切换节点展开状态,重新生成扁平化数组 toggleExpand(item: any) { item.expanded = !item.expanded; this.flattenedTree = this.flattenTree(this.root); } }
步骤2:简化模板,单虚拟滚动容器
<cdk-virtual-scroll-viewport style="height: 750px" itemSize="50"> <li *cdkVirtualFor="let item of flattenedTree; trackBy: trackById" [style.padding-left.px]="item.level * 20"> <button (click)="toggleExpand(item)"> {{ item.expanded ? '-' : '+' }} {{ item.id }} </button> </li> </cdk-virtual-scroll-viewport>
这种方案的优势是性能更高,适合大量节点的场景,缺点是需要修改数据格式,子节点的布局样式需要通过层级来控制。
内容的提问来源于stack exchange,提问作者MortenMoulder
相关产品推荐
相关产品推荐

