Angular 14中MatTreeView无法显示子节点问题排查
问题:Angular NestedTreeView无法展示GitLab子组数据
使用Angular的NestedTreeView展示GitLab公开组及其子组数据,父节点正常显示,但子节点无法展示。已按TreeView要求映射数据,GitLab API返回的数组包含正确的父、子节点数据。
相关代码
data.ts
export interface Data { name: string; id?: string; children?: Data[] }
data.service.ts
@Injectable({ providedIn: 'root' }) export class DataService { constructor(public http: HttpClient) { } getEndpoint(id: string): string { return `https://gitlab.com/api/v4/groups/${id}/subgroups` } getRoot(): Observable<Data[]>{ return this.http.get<Data[]>(this.getEndpoint("9970")).pipe( map((res: any) => res.map((data: Data) => { return { name: data.name, id: data.id, } }) ) ) } getNodes(subid: string): Observable<Data[]> { return this.http.get<Data[]>(this.getEndpoint(subid)).pipe( map((res: any) => res.map((data: Data) => { return { name: data.name, id: data.id, } }) ) ) } }
sidebar.component.ts
@Injectable({ providedIn: 'root' }) @Component({ selector: 'app-sidebar', templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'] }) export class AppSidebarComponent implements OnDestroy,OnInit { mobileQuery: MediaQueryList; private _mobileQueryListener: () => void; result: Data[] = []; treeControl = new NestedTreeControl<Data>(node => node.children); dataSource = new MatTreeNestedDataSource<Data>(); constructor( changeDetectorRef: ChangeDetectorRef, media: MediaMatcher, public menuItems: MenuItems, public http: HttpClient, public dataService: DataService ) { this.mobileQuery = media.matchMedia('(min-width: 768px)'); this._mobileQueryListener = () => changeDetectorRef.detectChanges(); this.mobileQuery.addListener(this._mobileQueryListener); this.getRoots(); } ngOnInit(): void { } getRoots() { this.dataService.getRoot().subscribe(response => { this.result = response this.result.forEach(directory => { let nodeList: Data[]= [] this.dataService.getNodes(directory.id!).subscribe(response => { nodeList = <Data[]> response if(nodeList.length>0) directory.children = nodeList; nodeList = []; }) }); console.log(this.result); this.dataSource.data = this.result; }) } ngOnDestroy(): void { this.mobileQuery.removeListener(this._mobileQueryListener); } hasChild = (_: number, node: Data) => !!node.children && node.children.length>0; }
sidebar.component.html
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="example-tree"> <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle> {{node.name}} </mat-tree-node> <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild"> <div class="mat-tree-node"> <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}} </div> <div [class.example-tree-invisible]="!treeControl.isExpanded(node)" role="group"> <ng-container matTreeNodeOutlet></ng-container> </div> </mat-nested-tree-node> </mat-tree>
问题分析
核心原因是异步请求的时序问题:
- 在
getRoots方法中,设置this.dataSource.data = this.result的操作早于子组请求的完成时间,此时父节点的children属性尚未被赋值,TreeView初始化时无法识别子节点。 - 即使后续
children被赋值,由于没有主动触发变更检测,TreeView也不会自动更新视图。 - 另外,组件上错误添加了
@Injectable装饰器,组件仅需@Component装饰器即可,这可能引发不必要的错误。
解决方案
方案1:批量等待所有子组请求完成后更新数据源
使用forkJoin等待所有父节点的子组请求都完成后,再将完整的树形数据赋值给数据源,确保初始化时TreeView能获取到完整结构:
// 先移除组件上的@Injectable装饰器,仅保留@Component @Component({ selector: 'app-sidebar', templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'] }) export class AppSidebarComponent implements OnDestroy,OnInit { // ... 其他代码不变 ... constructor( private cdr: ChangeDetectorRef, // 改为私有便于内部使用 media: MediaMatcher, public menuItems: MenuItems, public http: HttpClient, public dataService: DataService ) { // ... 其他代码不变 ... } getRoots() { this.dataService.getRoot().subscribe(response => { // 为每个父节点创建获取子组的请求 const subGroupRequests = response.map(directory => this.dataService.getNodes(directory.id!).pipe( map(subGroups => { if(subGroups.length > 0) { directory.children = subGroups; } return directory; }) ) ); // 等待所有子组请求完成 forkJoin(subGroupRequests).subscribe(updatedRoots => { this.result = updatedRoots; this.dataSource.data = this.result; this.cdr.detectChanges(); // 确保视图更新 }); }); } // ... 其他代码不变 ... }
方案2:按需加载子节点(性能更优)
利用MatTree的交互特性,在用户点击展开父节点时才请求对应子组数据,避免一次性加载所有子组,同时解决异步时序问题:
- 修改组件逻辑:
// 移除组件上的@Injectable装饰器 @Component({ selector: 'app-sidebar', templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'] }) export class AppSidebarComponent implements OnDestroy,OnInit { // ... 其他代码不变 ... constructor( private cdr: ChangeDetectorRef, media: MediaMatcher, public menuItems: MenuItems, public http: HttpClient, public dataService: DataService ) { // ... 其他代码不变 ... this.getRoots(); } getRoots() { this.dataService.getRoot().subscribe(response => { this.result = response; this.dataSource.data = this.result; }); } // 点击展开按钮时加载子节点 loadChildren(node: Data) { if (!node.children) { // 避免重复加载 this.dataService.getNodes(node.id!).subscribe(subGroups => { node.children = subGroups; this.treeControl.expand(node); this.cdr.detectChanges(); // 触发视图更新 }); } } // 修改hasChild,确保所有父节点都显示展开按钮 hasChild = (_: number, node: Data) => true; // ... 其他代码不变 ... }
- 修改模板,绑定加载事件:
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="example-tree"> <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle> {{node.name}} </mat-tree-node> <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild"> <div class="mat-tree-node"> <button mat-icon-button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.name" (click)="loadChildren(node)"> <mat-icon class="mat-icon-rtl-mirror"> {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}} </mat-icon> </button> {{node.name}} </div> <div [class.example-tree-invisible]="!treeControl.isExpanded(node)" role="group"> <ng-container matTreeNodeOutlet></ng-container> </div> </mat-nested-tree-node> </mat-tree>
额外注意事项
- 组件不能同时使用
@Injectable和@Component装饰器,必须移除组件上的@Injectable。 - 使用
ChangeDetectorRef手动触发变更检测,确保异步数据更新后视图能及时刷新。
内容的提问来源于stack exchange,提问作者Ayoub Asrih
相关产品推荐
相关产品推荐

