Skip to content

Commit bf2bd8d

Browse files
committed
feat(tree): Add Cdk Tree component (#7984)
* Add Cdk Tree component * Put tree control in control folder. Removed NestedNode FlatNode types and add getLevel getChildren isExpandable in TreeControl. * Use static varaible to pass node data * fix lint * another round * update documents according to comments
1 parent c720198 commit bf2bd8d

23 files changed

+1519
-1
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
/src/cdk/stepper/** @mmalerba
7070
/src/cdk/table/** @andrewseguin
7171
/src/cdk/testing/** @devversion
72+
/src/cdk/tree/** @tinayuangao
7273

7374
# Moment adapter package
7475
/src/material-moment-adapter/** @mmalerba

src/cdk/collections/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export {
1414
UniqueSelectionDispatcherListener,
1515
UNIQUE_SELECTION_DISPATCHER_PROVIDER,
1616
} from './unique-selection-dispatcher';
17+
export * from './tree-adapter';

src/cdk/collections/tree-adapter.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {SelectionModel} from './selection';
10+
11+
12+
/**
13+
* Interface for a class that can flatten hierarchical structured data and re-expand the flattened
14+
* data back into its original structure. Should be used in conjunction with the cdk-tree.
15+
*/
16+
export interface TreeDataNodeFlattener<T> {
17+
/** Transforms a set of hierarchical structured data into a flattened data array. */
18+
flattenNodes(structuredData: any[]): T[];
19+
20+
/**
21+
* Expands a flattened array of data into its hierarchical form using the provided expansion
22+
* model.
23+
*/
24+
expandFlattenedNodes(nodes: T[], expansionModel: SelectionModel<T>): T[];
25+
26+
/**
27+
* Put node descendants of node in array.
28+
* If `onlyExpandable` is true, then only process expandable descendants.
29+
*/
30+
nodeDescendents(node: T, nodes: T[], onlyExpandable: boolean);
31+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {SelectionModel} from '@angular/cdk/collections';
9+
import {Observable} from 'rxjs/Observable';
10+
import {TreeControl} from './tree-control';
11+
12+
/** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */
13+
export abstract class BaseTreeControl<T> implements TreeControl<T> {
14+
15+
/** Gets a list of descendent data nodes of a subtree rooted at given data node recursively. */
16+
abstract getDescendants(dataNode: T): T[];
17+
18+
/** Expands all data nodes in the tree. */
19+
abstract expandAll(): void;
20+
21+
/** Saved data node for `expandAll` action. */
22+
dataNodes: T[];
23+
24+
/** A selection model with multi-selection to track expansion status. */
25+
expansionModel: SelectionModel<T> = new SelectionModel<T>(true);
26+
27+
/** Get depth of a given data node, return the level number. This is for flat tree node. */
28+
getLevel: (dataNode: T) => number;
29+
30+
/**
31+
* Whether the data node is expandable. Returns true if expandable.
32+
* This is for flat tree node.
33+
*/
34+
isExpandable: (dataNode: T) => boolean;
35+
36+
/** Gets a stream that emits whenever the given data node's children change. */
37+
getChildren: (dataNode: T) => Observable<T[]>;
38+
39+
/** Toggles one single data node's expanded/collapsed state. */
40+
toggle(dataNode: T): void {
41+
this.expansionModel.toggle(dataNode);
42+
}
43+
44+
/** Expands one single data node. */
45+
expand(dataNode: T): void {
46+
this.expansionModel.select(dataNode);
47+
}
48+
49+
/** Collapses one single data node. */
50+
collapse(dataNode: T): void {
51+
this.expansionModel.deselect(dataNode);
52+
}
53+
54+
/** Whether a given data node is expanded or not. Returns true if the data node is expanded. */
55+
isExpanded(dataNode: T): boolean {
56+
return this.expansionModel.isSelected(dataNode);
57+
}
58+
59+
/** Toggles a subtree rooted at `node` recursively. */
60+
toggleDescendants(dataNode: T): void {
61+
this.expansionModel.isSelected(dataNode)
62+
? this.collapseDescendants(dataNode)
63+
: this.expandDescendants(dataNode);
64+
}
65+
66+
/** Collapse all dataNodes in the tree. */
67+
collapseAll(): void {
68+
this.expansionModel.clear();
69+
}
70+
71+
/** Expands a subtree rooted at given data node recursively. */
72+
expandDescendants(dataNode: T): void {
73+
let toBeProcessed = [dataNode];
74+
toBeProcessed.push(...this.getDescendants(dataNode));
75+
this.expansionModel.select(...toBeProcessed);
76+
}
77+
78+
/** Collapses a subtree rooted at given data node recursively. */
79+
collapseDescendants(dataNode: T): void {
80+
let toBeProcessed = [dataNode];
81+
toBeProcessed.push(...this.getDescendants(dataNode));
82+
this.expansionModel.deselect(...toBeProcessed);
83+
}
84+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import {FlatTreeControl} from './flat-tree-control';
2+
3+
describe('CdkFlatTreeControl', () => {
4+
let treeControl: FlatTreeControl<TestData>;
5+
let getLevel = (node: TestData) => node.level;
6+
let isExpandable = (node: TestData) => node.children && node.children.length > 0;
7+
8+
beforeEach(() => {
9+
treeControl = new FlatTreeControl<TestData>(getLevel, isExpandable);
10+
});
11+
12+
describe('base tree control actions', () => {
13+
it('should be able to expand and collapse dataNodes', () => {
14+
const nodes = generateData(10, 4);
15+
const secondNode = nodes[1];
16+
const sixthNode = nodes[5];
17+
treeControl.dataNodes = nodes;
18+
19+
treeControl.expand(secondNode);
20+
21+
22+
expect(treeControl.isExpanded(secondNode))
23+
.toBeTruthy('Expect second node to be expanded');
24+
expect(treeControl.expansionModel.selected)
25+
.toContain(secondNode, 'Expect second node in expansionModel');
26+
expect(treeControl.expansionModel.selected.length)
27+
.toBe(1, 'Expect only second node in expansionModel');
28+
29+
treeControl.toggle(sixthNode);
30+
31+
expect(treeControl.isExpanded(secondNode))
32+
.toBeTruthy('Expect second node to stay expanded');
33+
expect(treeControl.isExpanded(sixthNode))
34+
.toBeTruthy('Expect sixth node to be expanded');
35+
expect(treeControl.expansionModel.selected)
36+
.toContain(sixthNode, 'Expect sixth node in expansionModel');
37+
expect(treeControl.expansionModel.selected)
38+
.toContain(secondNode, 'Expect second node in expansionModel');
39+
expect(treeControl.expansionModel.selected.length)
40+
.toBe(2, 'Expect two dataNodes in expansionModel');
41+
42+
treeControl.collapse(seconNode);
43+
44+
expect(treeControl.isExpanded(secondNode))
45+
.toBeFalsy('Expect second node to be collapsed');
46+
expect(treeControl.expansionModel.selected.length)
47+
.toBe(1, 'Expect one node in expansionModel');
48+
expect(treeControl.isExpanded(sixthNode)).toBeTruthy('Expect sixth node to stay expanded');
49+
expect(treeControl.expansionModel.selected)
50+
.toContain(sixthNode, 'Expect sixth node in expansionModel');
51+
});
52+
53+
it('should return correct expandable values', () => {
54+
const nodes = generateData(10, 4);
55+
treeControl.dataNodes = nodes;
56+
57+
for (let i = 0; i < 10; i++) {
58+
expect(treeControl.isExpandable(nodes[i]))
59+
.toBeTruthy(`Expect node[${i}] to be expandable`);
60+
61+
for (let j = 0; j < 4; j++) {
62+
expect(treeControl.isExpandable(nodes[i].children[j]))
63+
.toBeFalsy(`Expect node[${i}]'s child[${j}] to be not expandable`);
64+
}
65+
}
66+
});
67+
68+
it('should return correct levels', () => {
69+
const numNodes = 10;
70+
const numChildren = 4;
71+
const numGrandChildren = 2;
72+
const nodes = generateData(numNodes, numChildren, numGrandChildren);
73+
treeControl.dataNodes = nodes;
74+
75+
for (let i = 0; i < numNodes; i++) {
76+
expect(treeControl.getLevel(nodes[i]))
77+
.toBe(1, `Expec node[${i}]'s level to be 1`);
78+
79+
for (let j = 0; j < numChildren; j++) {
80+
expect(treeControl.getLevel(nodes[i].children[j]))
81+
.toBe(2, `Expect node[${i}]'s child[${j}] to be not expandable`);
82+
83+
for (let k = 0; k < numGrandChildren; k++) {
84+
expect(treeControl.getLevel(nodes[i].children[j].children[k]))
85+
.toBe(3, `Expect node[${i}]'s child[${j}] to be not expandable`);
86+
}
87+
}
88+
}
89+
});
90+
91+
it('should toggle descendants correctly', () => {
92+
const numNodes = 10;
93+
const numChildren = 4;
94+
const numGrandChildren = 2;
95+
const nodes = generateData(numNodes, numChildren, numGrandChildren);
96+
97+
let data = [];
98+
flatten(nodes, data);
99+
treeControl.dataNodes = data;
100+
101+
treeControl.expandDescendants(nodes[1]);
102+
103+
const expandedNodesNum = 1 + numChildren + numChildren * numGrandChildren;
104+
expect(treeControl.expansionModel.selected.length)
105+
.toBe(expandedNodesNum, `Expect expanded ${expandedNodesNum} nodes`);
106+
107+
expect(treeControl.isExpanded(nodes[1])).toBeTruthy('Expect second node to be expanded');
108+
for (let i = 0; i < numChildren; i++) {
109+
110+
expect(treeControl.isExpanded(nodes[1].children[i]))
111+
.toBeTruthy(`Expect second node's children to be expanded`);
112+
for (let j = 0; j < numGrandChildren; j++) {
113+
expect(treeControl.isExpanded(nodes[1].children[i].children[j]))
114+
.toBeTruthy(`Expect second node grand children to be not expanded`);
115+
}
116+
}
117+
118+
});
119+
120+
it('should be able to expand/collapse all the dataNodes', () => {
121+
const numNodes = 10;
122+
const numChildren = 4;
123+
const numGrandChildren = 2;
124+
const nodes = generateData(numNodes, numChildren, numGrandChildren);
125+
let data = [];
126+
flatten(nodes, data);
127+
treeControl.dataNodes = data;
128+
129+
treeControl.expandDescendants(nodes[1]);
130+
131+
treeControl.collapseAll();
132+
133+
expect(treeControl.expansionModel.selected.length).toBe(0, `Expect no expanded nodes`);
134+
135+
treeControl.expandAll();
136+
137+
const totalNumber = numNodes + numNodes * numChildren
138+
+ numNodes * numChildren * numGrandChildren;
139+
expect(treeControl.expansionModel.selected.length)
140+
.toBe(totalNumber, `Expect ${totalNumber} expanded nodes`);
141+
});
142+
});
143+
});
144+
145+
export class TestData {
146+
a: string;
147+
b: string;
148+
c: string;
149+
level: number;
150+
children: TestData[];
151+
152+
constructor(a: string, b: string, c: string, level: number = 1, children: TestData[] = []) {
153+
this.a = a;
154+
this.b = b;
155+
this.c = c;
156+
this.level = level;
157+
this.children = children;
158+
}
159+
}
160+
161+
function generateData(dataLength: number, childLength: number, grandChildLength: number = 0)
162+
: TestData[] {
163+
let data = <any>[];
164+
let nextIndex = 0;
165+
for (let i = 0; i < dataLength; i++) {
166+
let children = <any>[];
167+
for (let j = 0; j < childLength; j++) {
168+
let grandChildren = <any>[];
169+
for (let k = 0; k < grandChildLength; k++) {
170+
grandChildren.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 3));
171+
}
172+
children.push(
173+
new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 2, grandChildren));
174+
}
175+
data.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 1, children));
176+
}
177+
return data;
178+
}
179+
180+
function flatten(nodes: TestData[], data: TestData[]) {
181+
for (let node of nodes) {
182+
data.push(node);
183+
184+
if (node.children && node.children.length > 0) {
185+
flatten(node.children, data);
186+
}
187+
}
188+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {BaseTreeControl} from './base-tree-control';
10+
11+
/** Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */
12+
export class FlatTreeControl<T> extends BaseTreeControl<T> {
13+
14+
/** Construct with flat tree data node functions getLevel and isExpandable. */
15+
constructor(public getLevel: (dataNode: T) => number,
16+
public isExpandable: (dataNode: T) => boolean) {
17+
super();
18+
}
19+
20+
/**
21+
* Gets a list of the data node's subtree of descendent data nodes.
22+
*
23+
* To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes
24+
* with correct levels.
25+
*/
26+
getDescendants(dataNode: T): T[] {
27+
const startIndex = this.dataNodes.indexOf(dataNode);
28+
const results: T[] = [];
29+
30+
// Goes through flattened tree nodes in the `dataNodes` array, and get all descendants.
31+
// The level of descendants of a tree node must be greater than the level of the given
32+
// tree node.
33+
// If we reach a node whose level is equal to the level of the tree node, we hit a sibling.
34+
// If we reach a node whose level is greater than the level of the tree node, we hit a
35+
// sibling of an ancestor.
36+
for (let i = startIndex + 1;
37+
i < this.dataNodes.length && this.getLevel(dataNode) < this.getLevel(this.dataNodes[i]);
38+
i++) {
39+
results.push(this.dataNodes[i]);
40+
}
41+
return results;
42+
}
43+
44+
/**
45+
* Expands all data nodes in the tree.
46+
*
47+
* To make this working, the `dataNodes` variable of the TreeControl must be set to all flattened
48+
* data nodes of the tree.
49+
*/
50+
expandAll(): void {
51+
this.expansionModel.select(...this.dataNodes);
52+
}
53+
}

0 commit comments

Comments
 (0)