关于打字稿:Angular 6材质树折叠功能无法正常使用

Angular 6 Material Tree collapse functionality is not working properly

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

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

由于我开发的树无法正常工作,因此我按原样复制了上面的代码,然后尝试在我的计算机上运行。 但收合功能无法使用。 这是我的打字稿文件(html完全一样):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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; };


}

当我折叠具有多个子级的根节点时
这个结果将给出

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


为什么会这样

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

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

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

1
2
3
4
5
6
- 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将为2,这意味着splice函数将完全删除Child1OfChild2和Child2OfChild2(因为index + 1是Child1OfChild2的位置)。

但是说,例如,我们想从上方折叠示例树中的根。在这种情况下,索引+1将是Child1的位置,没关系。问题在于children.length将返回3,因为根只有三个直接子代。
这将导致删除从Child1开始的数组的前三个元素,导致Child2OfChild2和Child3仍在数组中。

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

1
2
3
4
5
6
7
8
9
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函数在最后一行中删除元素。