Skip to content

Commit a7b0c2d

Browse files
committed
feat(tree): support array of data as children in nested tree
1 parent 991daac commit a7b0c2d

File tree

9 files changed

+200
-28
lines changed

9 files changed

+200
-28
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: 11 additions & 6 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,15 @@ 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) {
46-
children.forEach((child: T) => this._getDescendants(descendants, child));
47-
}
48-
});
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 => {
49+
if (children && children.length > 0) {
50+
children.forEach((child: T) => this._getDescendants(descendants, child));
51+
}
52+
});
53+
}
4954
}
5055
}

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);

src/cdk/tree/tree.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,33 @@ describe('CdkTree', () => {
387387
});
388388
});
389389

390+
describe('with static children', () => {
391+
let fixture: ComponentFixture<StaticNestedCdkTreeApp>;
392+
let component: StaticNestedCdkTreeApp;
393+
394+
beforeEach(() => {
395+
configureCdkTreeTestingModule([StaticNestedCdkTreeApp]);
396+
fixture = TestBed.createComponent(StaticNestedCdkTreeApp);
397+
398+
component = fixture.componentInstance;
399+
dataSource = component.dataSource as FakeDataSource;
400+
tree = component.tree;
401+
treeElement = fixture.nativeElement.querySelector('cdk-tree');
402+
403+
fixture.detectChanges();
404+
});
405+
406+
it('with the right data', () => {
407+
expectNestedTreeToMatch(treeElement,
408+
[`topping_1 - cheese_1 + base_1`],
409+
[`topping_2 - cheese_2 + base_2`],
410+
[_, `topping_4 - cheese_4 + base_4`],
411+
[_, _, `topping_5 - cheese_5 + base_5`],
412+
[_, _, `topping_6 - cheese_6 + base_6`],
413+
[`topping_3 - cheese_3 + base_3`]);
414+
});
415+
});
416+
390417
describe('with when node', () => {
391418
let fixture: ComponentFixture<WhenNodeNestedCdkTreeApp>;
392419
let component: WhenNodeNestedCdkTreeApp;
@@ -825,6 +852,36 @@ class NestedCdkTreeApp {
825852
@ViewChild(CdkTree) tree: CdkTree<TestData>;
826853
}
827854

855+
@Component({
856+
template: `
857+
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
858+
<cdk-nested-tree-node *cdkTreeNodeDef="let node" class="customNodeClass">
859+
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
860+
<ng-template cdkTreeNodeOutlet></ng-template>
861+
</cdk-nested-tree-node>
862+
</cdk-tree>
863+
`
864+
})
865+
class StaticNestedCdkTreeApp {
866+
getChildren = (node: TestData) => node.children;
867+
868+
treeControl: TreeControl<TestData> = new NestedTreeControl(this.getChildren);
869+
870+
dataSource: FakeDataSource;
871+
872+
@ViewChild(CdkTree) tree: CdkTree<TestData>;
873+
874+
constructor() {
875+
const dataSource = new FakeDataSource(this.treeControl);
876+
const data = dataSource.data;
877+
const child = dataSource.addChild(data[1], false);
878+
dataSource.addChild(child, false);
879+
dataSource.addChild(child, false);
880+
881+
this.dataSource = dataSource;
882+
}
883+
}
884+
828885
@Component({
829886
template: `
830887
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">

src/cdk/tree/tree.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,25 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
9898
this._elementRef.nativeElement.focus();
9999
}
100100

101-
private _setRoleFromData(): void {
101+
protected _setRoleFromData(): void {
102102
if (this._tree.treeControl.isExpandable) {
103103
this.role = this._tree.treeControl.isExpandable(this._data) ? 'group' : 'treeitem';
104104
} else {
105105
if (!this._tree.treeControl.getChildren) {
106106
throw getTreeControlFunctionsMissingError();
107107
}
108-
this._tree.treeControl.getChildren(this._data).pipe(takeUntil(this._destroyed))
109-
.subscribe(children => {
110-
this.role = children && children.length ? 'group' : 'treeitem';
111-
});
108+
const childrenNodes = this._tree.treeControl.getChildren(this._data);
109+
if (Array.isArray(childrenNodes)) {
110+
this._setRoleFromChildren(childrenNodes as T[]);
111+
} else if (childrenNodes instanceof Observable) {
112+
childrenNodes.pipe(takeUntil(this._destroyed)).subscribe(this._setRoleFromChildren);
113+
}
112114
}
113115
}
116+
117+
protected _setRoleFromChildren(children: T[]) {
118+
this.role = children && children.length ? 'group' : 'treeitem';
119+
}
114120
}
115121

116122

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
@@ -51,25 +51,35 @@ export class MatTreeFlattener<T, F> {
5151
constructor(public transformFunction: (node: T, level: number) => F,
5252
public getLevel: (node: F) => number,
5353
public isExpandable: (node: F) => boolean,
54-
public getChildren: (node: T) => Observable<T[]>) {}
54+
public getChildren: (node: T) => Observable<T[]> | T[]) {}
5555

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

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

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

0 commit comments

Comments
 (0)