angulartreeangular-material-6

Angular Material Tree does not show child elements with array is populated


I am trying to dynamically load the children when the user expands a node.

The issue is when I populate the children array, mat-tree is not displaying the children. If I display the same data using simple *ngFor, when the children array has elements added, it shows them.

I have a working example here: stackblitz example This is the code and html

ts

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


    export class PropertyLevel {
       constructor(
        public code : string,
        public hasSubLevels: boolean,
        public subproperties : PropertyLevel[]
       ){}
    }

    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: [ './app.component.css' ]
    })
    export class AppComponent  {
      name = 'Angular';
        nestedTreeControl: NestedTreeControl<PropertyLevel>;
      nestedDataSource: MatTreeNestedDataSource<PropertyLevel>;

    constructor() {
        this.nestedTreeControl = new NestedTreeControl<PropertyLevel>(this._getChildren);
        this.nestedDataSource = new MatTreeNestedDataSource();

     this.nestedDataSource.data = [
      new PropertyLevel( '123', false, []),
      new PropertyLevel( '345', true, [
        new PropertyLevel( '345.a', false, null),
        new PropertyLevel( '345.b', true, []),
      ]),
      new PropertyLevel( '567', false,[]),
    ]; 
      } 

      hasNestedChild = (_: number, nodeData: PropertyLevel) => nodeData.subproperties;

      private _getChildren = (node: PropertyLevel) => node.subproperties;

      expandToggle(node: PropertyLevel, isExpanded: boolean): void {
        if (node.subproperties && node.subproperties.length == 0) {
          if(node.code == '123') {
            node.subproperties.push(new PropertyLevel('123.a', false, null))
          } 
          else if(node.code == '567') {
            node.subproperties.push(new PropertyLevel('567.a', false, null));
            node.subproperties.push(new PropertyLevel('567.b', false, null));
            node.subproperties.push(new PropertyLevel('567.c', false, null));
          } 
        }
      }
    }

html

    <mat-tree [dataSource]="nestedDataSource" [treeControl]="nestedTreeControl" class="example-tree">
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
        <li class="mat-tree-node">
          <button mat-icon-button disabled></button>
          {{node.code}}
        </li>
      </mat-tree-node>

      <mat-nested-tree-node *matTreeNodeDef="let node; when: hasNestedChild">
        <li>
          <div class="mat-tree-node">
            <button mat-icon-button matTreeNodeToggle
                  (click)="expandToggle(node, nestedTreeControl.isExpanded(node))"
                    [attr.aria-label]="'toggle ' + node.filename">
              <mat-icon class="mat-icon-rtl-mirror">
                {{nestedTreeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
              </mat-icon>
            </button>
            {{node.code}}
          </div>
          <ul [class.example-tree-invisible]="!nestedTreeControl.isExpanded(node)">
            <ng-container matTreeNodeOutlet></ng-container>
          </ul>
        </li>
      </mat-nested-tree-node>
    </mat-tree>
    <div>
      <ul>
        <li *ngFor="let node of nestedDataSource.data">
          {{node.code}}<br />
          <ul>
            <li *ngFor="let subnode of node.subproperties">
              {{subnode.code}}
            </li>
          </ul>
        </li>
      </ul>
    </div>

Solution

  • The moral to this story (please correct me if I'm wrong) is that in Angular vs Angularjs, or at least the Material Tree, rather than automatically wiring up change detection on everything, the developer must supply the change events, which reduces a lot of behind the scenes object creation, making Angular faster and leaner.

    So, the solution is to not use an array for the children, but rather a BehaviorSubject, and add a method to the class to addChild.

    I went back to the Tree with Nested Nodes (https://material.angular.io/components/tree/examples) example (https://stackblitz.com/angular/ngdvblkxajq), and tweaked the FileNode class and added a addChild and addChildren methods

    export class FileNode {
      kids: FileNode[] = [];
      children:BehaviorSubject<FileNode[]> = new BehaviorSubject<FileNode[]>(this.kids);
      filename: string;
      type: any;
      addChild(node:FileNode):void {
        this.kids.push(node);
        this.children.next(this.kids);
      }
      addchildren(nodes:FileNode[]) {
        this.kids = this.kids.concat(this.kids, nodes);
        this.children.next(this.kids);
      }
    }
    

    I then changed the line in buildFileTree that was setting the children, to call addChildren instead. node.children = this.buildFileTree(value, level + 1) became node.addchildren(this.buildFileTree(value, level + 1))

    I also added a method I could call from a button click to add a child to the picture node to test things out.

        addPictureFile():void {
        var picNode = this.data.find((node) => node.filename == 'Pictures');
        var newNode = new FileNode();
        newNode.filename = 'foo';
        newNode.type = 'gif';
        picNode.addChild(newNode);
      }
    

    Now, Material Tree did detect my change in children and updated itself. Working example https://stackblitz.com/edit/angular-addchildtonestedtree

    Complete, updated ts file:

    import {NestedTreeControl} from '@angular/cdk/tree';
    import {Component, Injectable} from '@angular/core';
    import {MatTreeNestedDataSource} from '@angular/material/tree';
    import {BehaviorSubject} from 'rxjs';
    
    /**
     * Json node data with nested structure. Each node has a filename and a value or a list of children
     */
    export class FileNode {
      kids: FileNode[] = [];
      children:BehaviorSubject<FileNode[]> = new BehaviorSubject<FileNode[]>(this.kids);
      filename: string;
      type: any;
      addChild(node:FileNode):void {
        this.kids.push(node);
        this.children.next(this.kids);
      }
      addchildren(nodes:FileNode[]) {
        this.kids = this.kids.concat(this.kids, nodes);
        this.children.next(this.kids);
      }
    }
    
    /**
     * The Json tree data in string. The data could be parsed into Json object
     */
    const TREE_DATA = JSON.stringify({
      Applications: {
        Calendar: 'app',
        Chrome: 'app',
        Webstorm: 'app'
      },
      Documents: {
        angular: {
          src: {
            compiler: 'ts',
            core: 'ts'
          }
        },
        material2: {
          src: {
            button: 'ts',
            checkbox: 'ts',
            input: 'ts'
          }
        }
      },
      Downloads: {
        October: 'pdf',
        November: 'pdf',
        Tutorial: 'html'
      },
      Pictures: {
        'Photo Booth Library': {
          Contents: 'dir',
          Pictures: 'dir'
        },
        Sun: 'png',
        Woods: 'jpg'
      }
    });
    
    /**
     * File database, it can build a tree structured Json object from string.
     * Each node in Json object represents a file or a directory. For a file, it has filename and type.
     * For a directory, it has filename and children (a list of files or directories).
     * The input will be a json object string, and the output is a list of `FileNode` with nested
     * structure.
     */
    @Injectable()
    export class FileDatabase {
      dataChange = new BehaviorSubject<FileNode[]>([]);
    
      get data(): FileNode[] { return this.dataChange.value; }
    
      constructor() {
        this.initialize();
      }
    
      initialize() {
        // Parse the string to json object.
        const dataObject = JSON.parse(TREE_DATA);
    
        // Build the tree nodes from Json object. The result is a list of `FileNode` with nested
        //     file node as children.
        const data = this.buildFileTree(dataObject, 0);
    
        // Notify the change.
        this.dataChange.next(data);
      }
    
      /**
       * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
       * The return value is the list of `FileNode`.
       */
      buildFileTree(obj: {[key: string]: any}, level: number): FileNode[] {
        return Object.keys(obj).reduce<FileNode[]>((accumulator, key) => {
          const value = obj[key];
          const node = new FileNode();
          node.filename = key;
    
          if (value != null) {
            if (typeof value === 'object') {
              node.addchildren(this.buildFileTree(value, level + 1));
            } else {
              node.type = value;
            }
          }
    
          return accumulator.concat(node);
        }, []);
      }
        addPictureFile():void {
        var picNode = this.data.find((node) => node.filename == 'Pictures');
        var newNode = new FileNode();
        newNode.filename = 'foo';
        newNode.type = 'gif';
        picNode.addChild(newNode);
      }
    }
    
    /**
     * @title Tree with nested nodes
     */
    @Component({
      selector: 'tree-nested-overview-example',
      templateUrl: 'tree-nested-overview-example.html',
      styleUrls: ['tree-nested-overview-example.css'],
      providers: [FileDatabase]
    })
    export class TreeNestedOverviewExample {
      nestedTreeControl: NestedTreeControl<FileNode>;
      nestedDataSource: MatTreeNestedDataSource<FileNode>;
    
      constructor(private database: FileDatabase) {
        this.nestedTreeControl = new NestedTreeControl<FileNode>(this._getChildren);
        this.nestedDataSource = new MatTreeNestedDataSource();
    
        database.dataChange.subscribe(data => {
        this.nestedDataSource.data = data;
        }
        );
      }
    
      hasNestedChild = (_: number, nodeData: FileNode) => !nodeData.type;
    
      private _getChildren = (node: FileNode) => node.children;
    }