typescript - Angular 6 Material 树折叠功能无法正常工作

标签 typescript treeview angular-material angular6

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

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

由于我开发的树不能正常工作,我原样复制了上面的代码并尝试在我的机器上运行。但折叠功能不起作用。这是我的 typescript 文件(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 个子级的根节点时 this result will be given

伙计们,有人能告诉我这是什么原因吗?我该如何解决?这将是一个很大的帮助。

最佳答案

为什么会这样

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

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

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

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

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

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

在上面的示例中折叠 Child2 时,这将正常工作:元素从位置 index+1 删除(索引是 Child2 的位置)。由于 Child2 有两个 child ,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 函数删除元素发生在最后一行。

关于typescript - Angular 6 Material 树折叠功能无法正常工作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53484824/

相关文章:

javascript - 对象数组减少 typescript 错误: not assignable to parameter of type 'never'

WPF DataBound TreeView 展开/折叠

angular - 禁止在 Angular Material 对话框区域外单击以关闭对话框(使用 Angular 版本 4.0+)

javascript - redux-saga-test-plan expectSaga- 使用状态分支测试 reducer

javascript - 如何将JavaScript文件导入angular2

ASP.NET TreeView 排序

angular - 使用数组内容过滤 MatTableDataSource

javascript - 动态组件列表上带有 ng-template 的 cdkDropList 不起作用

数组和对象类型的 typescript 联合

JavaFX:是否可以为整个 TreeView 设置背景颜色?