You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,
          }
        })
      )
    )
  } 
}
@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;

}
<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的交互特性,在用户点击展开父节点时才请求对应子组数据,避免一次性加载所有子组,同时解决异步时序问题:

  1. 修改组件逻辑:
// 移除组件上的@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;

  // ... 其他代码不变 ...
}
  1. 修改模板,绑定加载事件:
<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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 01:45:30