5

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>
1
  • 1
    I think it is more complicated than you think, check the dynamic data section in doc: material.angular.io/components/tree/examples. I think the data change does not trigger the UI change so you will need to implement the dynamic data source according to the example. Commented Dec 25, 2018 at 7:13

1 Answer 1

7

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;
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much for sharing the solution to the issue. I had exactly the same problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.