Angular 6材质树折叠功能无法正常工作

时间:2018-11-26 15:57:47

标签: typescript treeview angular-material angular6

当前,我正在尝试使用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; };


}

当我折叠具有多个子级的根节点时 this result will be given

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

1 个答案:

答案 0 :(得分:1)

为什么会发生

其原因是实现切换功能的方式。折叠节点时(对于 expand 参数调用toggleNode并带有false),将执行以下行:

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

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

- 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函数在最后一行中删除元素。