首页 > 解决方案 > Angular 6 Material Tree 折叠功能无法正常工作

问题描述

目前,我正在尝试使用 Angular 材质树组件开发动态数据的树结构,并遵循下面提到的代码示例:

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

由于我开发的树不能正常工作,我照原样复制了上面的代码并尝试在我的机器上运行。但折叠功能不起作用。这是我的打字稿文件(html完全相同):

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; };


}

当我折叠具有超过 1 个子级别的根节点时, 将给出此结果

所以各位大侠,谁能告诉我这是什么原因?我该如何解决?这将是一个很大的帮助。

标签: typescripttreeviewangular-materialangular6

解决方案


为什么会发生这种情况

原因是切换功能的实现方式。折叠节点时(toggleNode使用 false调用expand参数)会执行以下行:

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

在这种情况下,材质树使用的平面树数据结构将其所有元素存储在一个简单的数组中,以及每个节点的级别属性。因此,一棵树可能看起来像这样:

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

请注意,展开节点时,子元素在数组中放置在其父元素之后。当一个节点被折叠时,该节点的子元素应该从数组中移除。在这种情况下,这仅在没有子节点被扩展并因此本身具有子节点的情况下才有效。如果我们再看一下我上面提到的那行代码,就很清楚为什么会这样了:

折叠节点时,会调用上面的代码行。该splice函数从第一个参数中传递的位置开始删除一定数量的元素(index+1,这是我们要折叠的元素之后的第一个元素)。被移除的元素数量在第二个参数中传递(在本例中为children.length)。

当从上面的示例中折叠 Child2 时,这将正常工作:元素从位置index+1中删除(索引是 Child2 的位置)。由于 Child2 有两个孩子,children.length将为两个,这意味着该splice函数将准确删除 Child1OfChild2 和 Child2OfChild2(因为index+1是 Child1OfChild2 的位置)。

但是,例如,我们想从上面折叠示例树中的根。在这种情况下index+1将是 Child1 的位置,这很好。问题是children.length将返回 3,因为根只有三个直接子级。这将导致从 Child1 开始的数组的前三个元素被移除,导致 Child2OfChild2 和 Child3 仍然在数组中。

解决方案

我解决这个问题的方法是用以下逻辑替换有问题的代码行:

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);

在第一行中,我使用该slice函数在我们折叠的节点之后获取数组的一部分,直到数组的末尾。在此之后,我使用 for 循环来计算该子数组的元素数量,这些元素的级别高于我们正在折叠的节点(更高级别意味着它们是子节点或孙子节点等)。一旦循环遇到与我们正在折叠的节点具有相同级别的节点,循环将停止并且我们将获得计数,其中包含我们要从节点之后的第一个元素开始删除的元素数我们正在崩溃。元素的删除发生在使用该splice函数的最后一行。


推荐阅读