Skip to content

Commit 1b14996

Browse files
committed
feat(tree): support array of data as children in nested tree
1 parent 4166d16 commit 1b14996

File tree

12 files changed

+206
-34
lines changed

12 files changed

+206
-34
lines changed

src/cdk/tree/control/base-tree-control.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export abstract class BaseTreeControl<T> implements TreeControl<T> {
3434
isExpandable: (dataNode: T) => boolean;
3535

3636
/** Gets a stream that emits whenever the given data node's children change. */
37-
getChildren: (dataNode: T) => Observable<T[]>;
37+
getChildren: (dataNode: T) => (Observable<T[]> | T[]);
3838

3939
/** Toggles one single data node's expanded/collapsed state. */
4040
toggle(dataNode: T): void {

src/cdk/tree/control/nested-tree-control.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,95 @@ describe('CdkNestedTreeControl', () => {
9191
expect(treeControl.expansionModel.selected.length)
9292
.toBe(totalNumber, `Expect ${totalNumber} expanded nodes`);
9393
});
94+
95+
describe('with children array', () => {
96+
let getStaticChildren = (node: TestData) => node.children;
97+
98+
beforeEach(() => {
99+
treeControl = new NestedTreeControl<TestData>(getStaticChildren);
100+
});
101+
102+
it('should be able to expand and collapse dataNodes', () => {
103+
const nodes = generateData(10, 4);
104+
const node = nodes[1];
105+
const sixthNode = nodes[5];
106+
treeControl.dataNodes = nodes;
107+
108+
treeControl.expand(node);
109+
110+
111+
expect(treeControl.isExpanded(node)).toBeTruthy('Expect second node to be expanded');
112+
expect(treeControl.expansionModel.selected)
113+
.toContain(node, 'Expect second node in expansionModel');
114+
expect(treeControl.expansionModel.selected.length)
115+
.toBe(1, 'Expect only second node in expansionModel');
116+
117+
treeControl.toggle(sixthNode);
118+
119+
expect(treeControl.isExpanded(node)).toBeTruthy('Expect second node to stay expanded');
120+
expect(treeControl.expansionModel.selected)
121+
.toContain(sixthNode, 'Expect sixth node in expansionModel');
122+
expect(treeControl.expansionModel.selected)
123+
.toContain(node, 'Expect second node in expansionModel');
124+
expect(treeControl.expansionModel.selected.length)
125+
.toBe(2, 'Expect two dataNodes in expansionModel');
126+
127+
treeControl.collapse(node);
128+
129+
expect(treeControl.isExpanded(node)).toBeFalsy('Expect second node to be collapsed');
130+
expect(treeControl.expansionModel.selected.length)
131+
.toBe(1, 'Expect one node in expansionModel');
132+
expect(treeControl.isExpanded(sixthNode)).toBeTruthy('Expect sixth node to stay expanded');
133+
expect(treeControl.expansionModel.selected)
134+
.toContain(sixthNode, 'Expect sixth node in expansionModel');
135+
});
136+
137+
it('should toggle descendants correctly', () => {
138+
const numNodes = 10;
139+
const numChildren = 4;
140+
const numGrandChildren = 2;
141+
const nodes = generateData(numNodes, numChildren, numGrandChildren);
142+
treeControl.dataNodes = nodes;
143+
144+
treeControl.expandDescendants(nodes[1]);
145+
146+
const expandedNodesNum = 1 + numChildren + numChildren * numGrandChildren;
147+
expect(treeControl.expansionModel.selected.length)
148+
.toBe(expandedNodesNum, `Expect expanded ${expandedNodesNum} nodes`);
149+
150+
expect(treeControl.isExpanded(nodes[1])).toBeTruthy('Expect second node to be expanded');
151+
for (let i = 0; i < numChildren; i++) {
152+
153+
expect(treeControl.isExpanded(nodes[1].children[i]))
154+
.toBeTruthy(`Expect second node's children to be expanded`);
155+
for (let j = 0; j < numGrandChildren; j++) {
156+
expect(treeControl.isExpanded(nodes[1].children[i].children[j]))
157+
.toBeTruthy(`Expect second node grand children to be expanded`);
158+
}
159+
}
160+
});
161+
162+
it('should be able to expand/collapse all the dataNodes', () => {
163+
const numNodes = 10;
164+
const numChildren = 4;
165+
const numGrandChildren = 2;
166+
const nodes = generateData(numNodes, numChildren, numGrandChildren);
167+
treeControl.dataNodes = nodes;
168+
169+
treeControl.expandDescendants(nodes[1]);
170+
171+
treeControl.collapseAll();
172+
173+
expect(treeControl.expansionModel.selected.length).toBe(0, `Expect no expanded nodes`);
174+
175+
treeControl.expandAll();
176+
177+
const totalNumber = numNodes + (numNodes * numChildren)
178+
+ (numNodes * numChildren * numGrandChildren);
179+
expect(treeControl.expansionModel.selected.length)
180+
.toBe(totalNumber, `Expect ${totalNumber} expanded nodes`);
181+
});
182+
});
94183
});
95184
});
96185

src/cdk/tree/control/nested-tree-control.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {BaseTreeControl} from './base-tree-control';
1313
export class NestedTreeControl<T> extends BaseTreeControl<T> {
1414

1515
/** Construct with nested tree function getChildren. */
16-
constructor(public getChildren: (dataNode: T) => Observable<T[]>) {
16+
constructor(public getChildren: (dataNode: T) => (Observable<T[]> | T[])) {
1717
super();
1818
}
1919

@@ -41,10 +41,13 @@ export class NestedTreeControl<T> extends BaseTreeControl<T> {
4141
/** A helper function to get descendants recursively. */
4242
protected _getDescendants(descendants: T[], dataNode: T): void {
4343
descendants.push(dataNode);
44-
this.getChildren(dataNode).pipe(take(1)).subscribe(children => {
45-
if (children && children.length > 0) {
44+
const childrenNodes = this.getChildren(dataNode);
45+
if (Array.isArray(childrenNodes)) {
46+
childrenNodes.forEach((child: T) => this._getDescendants(descendants, child));
47+
} else if (childrenNodes instanceof Observable) {
48+
childrenNodes.pipe(take(1)).subscribe(children => {
4649
children.forEach((child: T) => this._getDescendants(descendants, child));
47-
}
48-
});
50+
});
51+
}
4952
}
5053
}

src/cdk/tree/control/tree-control.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,5 @@ export interface TreeControl<T> {
6060
readonly isExpandable: (dataNode: T) => boolean;
6161

6262
/** Gets a stream that emits whenever the given data node's children change. */
63-
readonly getChildren: (dataNode: T) => Observable<T[]>;
63+
readonly getChildren: (dataNode: T) => Observable<T[]> | T[];
6464
}

src/cdk/tree/nested-node.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
OnDestroy,
1616
QueryList,
1717
} from '@angular/core';
18+
import {Observable} from 'rxjs';
1819
import {takeUntil} from 'rxjs/operators';
1920

2021
import {CdkTree, CdkTreeNode} from './tree';
@@ -73,11 +74,13 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
7374
if (!this._tree.treeControl.getChildren) {
7475
throw getTreeControlFunctionsMissingError();
7576
}
76-
this._tree.treeControl.getChildren(this.data).pipe(takeUntil(this._destroyed))
77-
.subscribe(result => {
78-
this._children = result;
79-
this.updateChildrenNodes();
80-
});
77+
const childrenNodes = this._tree.treeControl.getChildren(this.data);
78+
if (Array.isArray(childrenNodes)) {
79+
this.updateChildrenNodes(childrenNodes as T[]);
80+
} else if (childrenNodes instanceof Observable) {
81+
childrenNodes.pipe(takeUntil(this._destroyed))
82+
.subscribe(result => this.updateChildrenNodes(result));
83+
}
8184
this.nodeOutlet.changes.pipe(takeUntil(this._destroyed))
8285
.subscribe(() => this.updateChildrenNodes());
8386
}
@@ -88,7 +91,10 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
8891
}
8992

9093
/** Add children dataNodes to the NodeOutlet */
91-
protected updateChildrenNodes(): void {
94+
protected updateChildrenNodes(children?: T[]): void {
95+
if (children) {
96+
this._children = children;
97+
}
9298
if (this.nodeOutlet.length && this._children) {
9399
const viewContainer = this.nodeOutlet.first.viewContainer;
94100
this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data);

src/cdk/tree/tree.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,33 @@ describe('CdkTree', () => {
478478
});
479479
});
480480

481+
describe('with static children', () => {
482+
let fixture: ComponentFixture<StaticNestedCdkTreeApp>;
483+
let component: StaticNestedCdkTreeApp;
484+
485+
beforeEach(() => {
486+
configureCdkTreeTestingModule([StaticNestedCdkTreeApp]);
487+
fixture = TestBed.createComponent(StaticNestedCdkTreeApp);
488+
489+
component = fixture.componentInstance;
490+
dataSource = component.dataSource as FakeDataSource;
491+
tree = component.tree;
492+
treeElement = fixture.nativeElement.querySelector('cdk-tree');
493+
494+
fixture.detectChanges();
495+
});
496+
497+
it('with the right data', () => {
498+
expectNestedTreeToMatch(treeElement,
499+
[`topping_1 - cheese_1 + base_1`],
500+
[`topping_2 - cheese_2 + base_2`],
501+
[_, `topping_4 - cheese_4 + base_4`],
502+
[_, _, `topping_5 - cheese_5 + base_5`],
503+
[_, _, `topping_6 - cheese_6 + base_6`],
504+
[`topping_3 - cheese_3 + base_3`]);
505+
});
506+
});
507+
481508
describe('with when node', () => {
482509
let fixture: ComponentFixture<WhenNodeNestedCdkTreeApp>;
483510
let component: WhenNodeNestedCdkTreeApp;
@@ -1073,6 +1100,36 @@ class NestedCdkTreeApp {
10731100
@ViewChild(CdkTree) tree: CdkTree<TestData>;
10741101
}
10751102

1103+
@Component({
1104+
template: `
1105+
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
1106+
<cdk-nested-tree-node *cdkTreeNodeDef="let node" class="customNodeClass">
1107+
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
1108+
<ng-template cdkTreeNodeOutlet></ng-template>
1109+
</cdk-nested-tree-node>
1110+
</cdk-tree>
1111+
`
1112+
})
1113+
class StaticNestedCdkTreeApp {
1114+
getChildren = (node: TestData) => node.children;
1115+
1116+
treeControl: TreeControl<TestData> = new NestedTreeControl(this.getChildren);
1117+
1118+
dataSource: FakeDataSource;
1119+
1120+
@ViewChild(CdkTree) tree: CdkTree<TestData>;
1121+
1122+
constructor() {
1123+
const dataSource = new FakeDataSource(this.treeControl);
1124+
const data = dataSource.data;
1125+
const child = dataSource.addChild(data[1], false);
1126+
dataSource.addChild(child, false);
1127+
dataSource.addChild(child, false);
1128+
1129+
this.dataSource = dataSource;
1130+
}
1131+
}
1132+
10761133
@Component({
10771134
template: `
10781135
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">

src/cdk/tree/tree.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
getTreeNoValidDataSourceError
4141
} from './tree-errors';
4242

43-
4443
/**
4544
* CDK tree component that connects with a data source to retrieve data of type `T` and renders
4645
* dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source.
@@ -338,17 +337,24 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
338337
this._elementRef.nativeElement.focus();
339338
}
340339

341-
private _setRoleFromData(): void {
340+
protected _setRoleFromData(): void {
342341
if (this._tree.treeControl.isExpandable) {
343342
this.role = this._tree.treeControl.isExpandable(this._data) ? 'group' : 'treeitem';
344343
} else {
345344
if (!this._tree.treeControl.getChildren) {
346345
throw getTreeControlFunctionsMissingError();
347346
}
348-
this._tree.treeControl.getChildren(this._data).pipe(takeUntil(this._destroyed))
349-
.subscribe(children => {
350-
this.role = children && children.length ? 'group' : 'treeitem';
351-
});
347+
const childrenNodes = this._tree.treeControl.getChildren(this._data);
348+
if (Array.isArray(childrenNodes)) {
349+
this._setRoleFromChildren(childrenNodes as T[]);
350+
} else if (childrenNodes instanceof Observable) {
351+
childrenNodes.pipe(takeUntil(this._destroyed))
352+
.subscribe(children => this._setRoleFromChildren(children));
353+
}
352354
}
353355
}
356+
357+
protected _setRoleFromChildren(children: T[]) {
358+
this.role = children && children.length ? 'group' : 'treeitem';
359+
}
354360
}

src/demo-app/tree/tree-demo.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
MatTreeFlattener,
1414
MatTreeNestedDataSource
1515
} from '@angular/material/tree';
16-
import {Observable, of as ofObservable} from 'rxjs';
1716
import {FileDatabase, FileFlatNode, FileNode} from './file-database';
1817

1918

@@ -69,7 +68,7 @@ export class TreeDemo {
6968

7069
isExpandable = (node: FileFlatNode) => { return node.expandable; };
7170

72-
getChildren = (node: FileNode): Observable<FileNode[]> => { return ofObservable(node.children); };
71+
getChildren = (node: FileNode): FileNode[] => { return node.children; };
7372

7473
hasChild = (_: number, _nodeData: FileFlatNode) => { return _nodeData.expandable; };
7574

src/lib/tree/data-source/flat-data-source.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,35 @@ export class MatTreeFlattener<T, F> {
5050
constructor(public transformFunction: (node: T, level: number) => F,
5151
public getLevel: (node: F) => number,
5252
public isExpandable: (node: F) => boolean,
53-
public getChildren: (node: T) => Observable<T[]>) {}
53+
public getChildren: (node: T) => Observable<T[]> | T[]) {}
5454

5555
_flattenNode(node: T, level: number,
5656
resultNodes: F[], parentMap: boolean[]): F[] {
5757
const flatNode = this.transformFunction(node, level);
5858
resultNodes.push(flatNode);
5959

6060
if (this.isExpandable(flatNode)) {
61-
this.getChildren(node).pipe(take(1)).subscribe(children => {
62-
children.forEach((child, index) => {
63-
let childParentMap: boolean[] = parentMap.slice();
64-
childParentMap.push(index != children.length - 1);
65-
this._flattenNode(child, level + 1, resultNodes, childParentMap);
61+
const childrenNodes = this.getChildren(node);
62+
if (Array.isArray(childrenNodes)) {
63+
this._flattenChildren(childrenNodes, level, resultNodes, parentMap);
64+
} else {
65+
childrenNodes.pipe(take(1)).subscribe(children => {
66+
this._flattenChildren(children, level, resultNodes, parentMap);
6667
});
67-
});
68+
}
6869
}
6970
return resultNodes;
7071
}
7172

73+
_flattenChildren(children: T[], level: number,
74+
resultNodes: F[], parentMap: boolean[]): void {
75+
children.forEach((child, index) => {
76+
let childParentMap: boolean[] = parentMap.slice();
77+
childParentMap.push(index != children.length - 1);
78+
this._flattenNode(child, level + 1, resultNodes, childParentMap);
79+
});
80+
}
81+
7282
/**
7383
* Flatten a list of node type T to flattened version of node F.
7484
* Please note that type T may be nested, and the length of `structuredData` may be different

src/lib/tree/tree.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ rendered data (such as expand/collapse) should be propagated through the table's
6363

6464
The `TreeControl` controls the expand/collapse state of tree nodes. Users can expand/collapse a tree
6565
node recursively through tree control. For nested tree node, `getChildren` function need to pass to
66-
the `NestedTreeControl` to make it work recursively. For flattened tree node, `getLevel` and
67-
`isExpandable` functions need to pass to the `FlatTreeControl` to make it work recursively.
66+
the `NestedTreeControl` to make it work recursively. The `getChildren` function may return an
67+
observable of children for a given node, or an array of children.
68+
For flattened tree node, `getLevel` and `isExpandable` functions need to pass to the
69+
`FlatTreeControl` to make it work recursively.
6870

6971
### Toggle
7072

src/material-examples/tree-checklist/tree-checklist-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {SelectionModel} from '@angular/cdk/collections';
22
import {FlatTreeControl} from '@angular/cdk/tree';
33
import {Component, Injectable} from '@angular/core';
44
import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree';
5-
import {BehaviorSubject, Observable, of as observableOf} from 'rxjs';
5+
import {BehaviorSubject} from 'rxjs';
66

77
/**
88
* Node for to-do item
@@ -146,7 +146,7 @@ export class TreeChecklistExample {
146146

147147
isExpandable = (node: TodoItemFlatNode) => node.expandable;
148148

149-
getChildren = (node: TodoItemNode): Observable<TodoItemNode[]> => observableOf(node.children);
149+
getChildren = (node: TodoItemNode): TodoItemNode[] => node.children;
150150

151151
hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable;
152152

src/material-examples/tree-nested-overview/tree-nested-overview-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {NestedTreeControl} from '@angular/cdk/tree';
22
import {Component, Injectable} from '@angular/core';
33
import {MatTreeNestedDataSource} from '@angular/material/tree';
4-
import {BehaviorSubject, of as observableOf} from 'rxjs';
4+
import {BehaviorSubject} from 'rxjs';
55

66
/**
77
* Json node data with nested structure. Each node has a filename and a value or a list of children
@@ -125,5 +125,5 @@ export class TreeNestedOverviewExample {
125125

126126
hasNestedChild = (_: number, nodeData: FileNode) => !nodeData.type;
127127

128-
private _getChildren = (node: FileNode) => observableOf(node.children);
128+
private _getChildren = (node: FileNode) => node.children;
129129
}

0 commit comments

Comments
 (0)