Angular 6 Material Tree collapse functionality is not working properly

Currently I'm trying to develop a tree structure for dynamic data using Angular material tree component and I followed the code example mention below:

https://stackblitz.com/edit/material-tree-dynamic

Since the tree which I have developed is not working properly, I copied above code as it is and try to run in my machine. but the collapse functionality is not working. Here is my typescript file (html is exactly the same):

import {Component, Injectable} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Observable} from 'rxjs/Observable';
import {merge} from 'rxjs/observable/merge';
import {map} from 'rxjs/operators/map';


/** Flat node with expandable and level information */
export class DynamicFlatNode {
  constructor(public item: string, public level: number = 1, public expandable: boolean = false, public isLoading: boolean = false) {}
}

/**
 * Database for dynamic data. When expanding a node in the tree, the data source will need to fetch
 * the descendants data from the database.
 */
export class DynamicDatabase {
  dataMap = new Map([
    ['Simulation', ['Factorio', 'Oxygen not included']],
    ['Indie', [`Don't Starve`, 'Terraria', 'Starbound', 'Dungeon of the Endless']],
    ['Action', ['Overcooked']],
    ['Strategy', ['Rise to ruins']],
    ['RPG', ['Magicka']],
    ['Magicka', ['Magicka 1', 'Magicka 2']],
    [`Don't Starve`, ['Region of Giants', 'Together', 'Shipwrecked']]
  ]);

  rootLevelNodes = ['Simulation', 'Indie', 'Action', 'Strategy', 'RPG'];

  /** Initial data from database */
  initialData(): DynamicFlatNode[] {
    return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 0, true));
  }


  getChildren(node: string): string[] | undefined {
    return this.dataMap.get(node);
  }

  isExpandable(node: string): boolean {
    return this.dataMap.has(node);
  }
}
/**
 * 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 DynamicDataSource {

  dataChange: BehaviorSubject<DynamicFlatNode[]> = new BehaviorSubject<DynamicFlatNode[]>([]);

  get data(): DynamicFlatNode[] { return this.dataChange.value; }
  set data(value: DynamicFlatNode[]) {
    this.treeControl.dataNodes = value;
    this.dataChange.next(value);
  }

  constructor(private treeControl: FlatTreeControl<DynamicFlatNode>,
              private database: DynamicDatabase) {}

  connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> {
    this.treeControl.expansionModel.onChange!.subscribe(change => {
      if ((change as SelectionChange<DynamicFlatNode>).added ||
        (change as SelectionChange<DynamicFlatNode>).removed) {
        this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
      }
    });

    return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
  }

  /** Handle expand/collapse behaviors */
  handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
    if (change.added) {
      change.added.forEach((node) => this.toggleNode(node, true));
    }
    if (change.removed) {
      change.removed.reverse().forEach((node) => this.toggleNode(node, false));
    }
  }

  /**
   * Toggle the node, remove from display list
   */
  toggleNode(node: DynamicFlatNode, expand: boolean) {
    const children = this.database.getChildren(node.item);
    const index = this.data.indexOf(node);
    if (!children || index < 0) { // If no children, or cannot find the node, no op
      return;
    }


    if (expand) {
      node.isLoading = true;

      setTimeout(() => {
        const nodes = children.map(name =>
          new DynamicFlatNode(name, node.level + 1, this.database.isExpandable(name)));
        this.data.splice(index + 1, 0, ...nodes);
        // notify the change
        this.dataChange.next(this.data);
        node.isLoading = false;
      }, 1000);
    } else {
      this.data.splice(index + 1, children.length);
      this.dataChange.next(this.data);
    }
  }
}

@Component({
  selector: 'app-audience-tree',
  templateUrl: './audience-tree.component.html',
  styleUrls: ['./audience-tree.component.css'],
  providers: [DynamicDatabase]
})
export class AudienceTreeComponent{

  constructor(database: DynamicDatabase) {
    this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
    this.dataSource = new DynamicDataSource(this.treeControl, database);

    this.dataSource.data = database.initialData();
  }

  treeControl: FlatTreeControl<DynamicFlatNode>;

  dataSource: DynamicDataSource;

  getLevel = (node: DynamicFlatNode) => { return node.level; };

  isExpandable = (node: DynamicFlatNode) => { return node.expandable; };

  hasChild = (_: number, _nodeData: DynamicFlatNode) => { return _nodeData.expandable; };


}

When I collapse a root node which has more than 1 children level this result will be given

So guys, can anybody tell me what is the reason for that? And how can I fix that? It would be a great help.

Upvotes: 3

Views: 4997

Answers (1)

Cines of Lode
Cines of Lode

Reputation: 129

Why this happens

The reason for this is the way the toggle function is implemented. When collapsing a node (calling toggleNode with false for expand parameter ) the following line is executed:

this.data.splice(index + 1, children.length);

The Flat Tree data structure used in this case for the Material Tree stores all its elements in a simple array, along with a level attribute for each node. Thus a tree might look like this:

- Root (lvl: 1)
- Child1 (lvl: 2)
- Child2 (lvl: 2)
- Child1OfChild2 (lvl: 3)
- Child2OfChild2 (lvl: 3)
- Child3 (lvl: 2)

Notice that the child elements are placed after their parent in the array when expanding the node. When a node is collapsed, the child elements of the node should be removed from the array. In this case this works only if none of the children is expanded and has thus children itself. It is clear why this is this way if we look again at the line of code I mentioned above:

When collapsing a node the line of code from above is called. The splice function removes a certain number of elements beginning from the position which is passed in the first parameter (index+1, which is the first element after the one we're collapsing). The number of elements which are removed is passed in the second parameter (children.length, in this case).

When collapsing Child2 in the example from above, this will work fine: The elements are removed from position index+1 (index being the position of Child2). As Child2 has two children, children.length will be two, which means that the splice function will remove exactly Child1OfChild2 and Child2OfChild2 (as index+1 is the position of Child1OfChild2).

But say for example we want to collapse the root in the example tree from above. In this case index+1 will be the position of Child1 which is alright. The problem is that children.length will return 3, as the root has only three direct children. This will lead to the removal of the first three elements of the array starting from Child1, resulting in Child2OfChild2 and Child3 still being in the array.

Solution

The way I solved this problem is by replacing the problematic line of code with following logic:

const afterCollapsed: ArtifactNode[] = this.data.slice(index + 1, this.data.length);
let count = 0;
for (count; count < afterCollapsed.length; count++) {
    const tmpNode = afterCollapsed[count];
    if (tmpNode.level <= node.level){
        break;
    }
}
this.data.splice(index+1, count);

In the first line i use the slice function to get the part of the array after the node we're collapsing until the end of the array. After this, I use a for-loop to count the number of elements for this sub-array which have a level higher than the node we're collapsing (higher level means they are children, or grand-children etc.). As soon as the loop will encounter a node which has the same level as the one we're collapsing the loop will stop and we will have the count, which contains the number of elements we want to remove beginning from the first element after the node we're collapsing. The removing of the elements happens in the last line using the splice function.

Upvotes: 2

Related Questions