angularangular-material

Mat-tree not showing dynamically loaded children[]


Been messing with this for way too long. I am gonna show some code that's overly complicated because I wanted to exclude some potential culprits.

Project on stackblitz. Start it with npm start.

My Angular component:

import { Component, OnInit } from '@angular/core';
import { EsrsDocumentService } from '../esrs-document.service';
import { Router, ActivatedRoute } from '@angular/router';
import { EsrsNode, EsrsTreeNode, SectionNode } from '../esrs-tree-node.model';

import {NestedTreeControl} from '@angular/cdk/tree';
import {MatTreeNestedDataSource} from '@angular/material/tree';


@Component({
  selector: 'app-esrs-document-detail',
  templateUrl: './esrs-document-detail.component.html',
  styleUrls: ['./esrs-document-detail.component.css']
})
export class EsrsDocumentDetailComponent implements OnInit {

  treeControl = new NestedTreeControl<EsrsTreeNode>(node => node.children);
  dataSource = new MatTreeNestedDataSource<EsrsTreeNode>();

  constructor(
    private esrsDocumentService: EsrsDocumentService,
    private activatedRoute: ActivatedRoute 
  ){}

  ngOnInit(): void {
  const docId = this.activatedRoute.snapshot.paramMap.get('id');
  // init datasource.data with esrs nodes
  this.esrsDocumentService.getEsrsNodes(docId).subscribe((nodes: EsrsNode[]) => {
    console.log("EsrsDocumentDetailComponent ngOnInit", nodes);
    this.dataSource.data = nodes.map(n => ({
      ...n,
      hasChild: n.hasChild,
      children: []
    }));
  });
}

loadChildren(node: EsrsNode) {
  if (node.children && node.children.length > 0) return;

  const docId = this.activatedRoute.snapshot.paramMap.get('id');
  this.esrsDocumentService.getSectionNodes(docId, node.id)
    .subscribe((sections: SectionNode[]) => {
      const treeNode = this.findNodeById(this.dataSource.data, node.id);
      if (treeNode) {
        // Cambia la referenza dei figli
        treeNode.children = sections.map(section => ({
          ...section,
          hasChild: false,
          children: []
        }));

        // Cambia la referenza del nodo padre nell'array radice
        const idx = this.dataSource.data.findIndex(n => n.id === treeNode.id);
        if (idx !== -1) {
          this.dataSource.data[idx] = { ...treeNode };
        }

        this.treeControl.dataNodes = this.dataSource.data;
        this.treeControl.expand(treeNode);
        this.dataSource.data = this.dataSource.data.slice();
      }
    });
}

// Trova il nodo nell'albero per id (ricorsivo)
private findNodeById(nodes: EsrsTreeNode[], id: string): EsrsTreeNode | null {
  for (const node of nodes) {
    if (node.id === id) return node;
    if (node.children && node.children.length > 0) {
      const found = this.findNodeById(node.children, id);
      if (found) return found;
    }
  }
  return null;
}
trackById(index: number, node: any): string {
  return node.id;
}

hasChild = (_: number, node: EsrsTreeNode) => !!(node.children && node.children.length > 0 || node.hasChild);

}

then its HTML file, please note the initial <pre>{{ dataSource.data | json }}</pre>

<pre>{{ dataSource.data | json }}</pre>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="example-tree"  [trackBy]="trackById">
  <!-- Nodo foglia -->
  <mat-tree-node *matTreeNodeDef="let node">
    {{ node.section || node.label || node.esrs }}
  </mat-tree-node>

  <!-- Nodo espandibile -->
  <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
    <div class="mat-tree-node">
      <button mat-icon-button matTreeNodeToggle
              [attr.aria-label]="'Toggle ' + (node.section || node.label || node.esrs)"
              (click)="loadChildren(node)">
        <mat-icon class="mat-icon-rtl-mirror">
          {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
        </mat-icon>
      </button>
      {{ node.section || node.label || node.esrs }} 
      
    </div>
    <div role="group">
      <ng-container matTreeNodeOutlet></ng-container>
    </div>
  </mat-nested-tree-node>
</mat-tree>

Now the first screenshot before expanding the node and then what's shown after expanding: before expanding

after expanding

As you can the children[] is perfectly pupulated however but they are not being shown as part pf <mat-tree>. Here is the HTML taken from the page:

<mat-tree
    _ngcontent-kya-c262=""
    role="tree"
    class="mat-tree cdk-tree example-tree"
    ng-reflect-data-source="[object Object]"
    ng-reflect-tree-control="[object Object]"
    ng-reflect-track-by="trackById(index, node) {
    "
>
    <mat-nested-tree-node _ngcontent-kya-c262="" class="cdk-tree-node cdk-nested-tree-node mat-nested-tree-node ng-star-inserted" role="treeitem" aria-level="1" aria-expanded="true">
        <div _ngcontent-kya-c262="" class="mat-tree-node">
            <button _ngcontent-kya-c262="" mat-icon-button="" mattreenodetoggle="" class="mat-focus-indicator mat-icon-button mat-button-base" aria-label="Toggle ESRS E1 - Cambiamento climatico">
                <span class="mat-button-wrapper">
                    <mat-icon _ngcontent-kya-c262="" role="img" class="mat-icon notranslate mat-icon-rtl-mirror material-icons mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font"> expand_more </mat-icon>
                </span>
                <span matripple="" class="mat-ripple mat-button-ripple mat-button-ripple-round" ng-reflect-disabled="false" ng-reflect-centered="true" ng-reflect-trigger="[object HTMLButtonElement]"></span>
                <span class="mat-button-focus-overlay"></span>
            </button>
            ESRS E1 - Cambiamento climatico
        </div>
        <div _ngcontent-kya-c262="" role="group"><!--ng-container--></div>
    </mat-nested-tree-node>
    <!--ng-container-->
</mat-tree>

why the hell am I not seeing no <mat-tree-node> at all?!


Solution

  • I was able to get the desired output by replacing
    this.dataSource.data.slice();
    with
    JSON.parse(JSON.stringify(this.dataSource.data));,
    which performs a deep copy.
    I also changed the HTML from:

    <div role="group">
      <ng-container matTreeNodeOutlet></ng-container>
    </div>
    

    to:

    <div role="group" *ngIf="treeControl.isExpanded(node)">
      <ng-container matTreeNodeOutlet></ng-container>
    </div>
    

    to conditionally render the child nodes only when the current node is expanded.

    treeControl.expand(node) doesn’t work because the node you pass isn’t the same instance Angular’s tree control uses. To fix it, change the function as follows:

    loadChildren(node: EsrsNode) {
        if (node.children && node.children.length > 0) return;
        this.esrsDocumentService
          .getSectionNodes('doc1', node.id)
          .subscribe((sections) => {
            node.children = sections;
            this.dataSource.data = JSON.parse(JSON.stringify(this.dataSource.data));
            this.treeControl.expand(
              this.dataSource.data.find((n) => n.id === node.id)
            );
          });
      }
    

    If the problem still isn't solved, you can check this StackOverflow link for help:

    How to update nested mat-tree dynamically