Skip to content

Commit d070d01

Browse files
BobobUnicornandrewseguin
authored andcommitted
feat(cdk/tree): implement the various expansion-related methods
1 parent 14374ad commit d070d01

File tree

4 files changed

+159
-29
lines changed

4 files changed

+159
-29
lines changed

src/cdk/tree/nested-node.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,15 @@ export class CdkNestedTreeNode<T, K = T>
6969

7070
ngAfterContentInit() {
7171
this._dataDiffer = this._differs.find([]).create(this._tree.trackBy);
72-
if (!this._tree.treeControl?.getChildren && (typeof ngDevMode === 'undefined' || ngDevMode)) {
72+
const childrenAccessor = this._tree._getChildrenAccessor();
73+
if (!childrenAccessor && (typeof ngDevMode === 'undefined' || ngDevMode)) {
7374
throw getTreeControlFunctionsMissingError();
74-
} else if (this._tree.treeControl?.getChildren) {
75-
const childrenNodes = this._tree.treeControl.getChildren(this.data);
76-
if (Array.isArray(childrenNodes)) {
77-
this.updateChildrenNodes(childrenNodes as T[]);
78-
} else if (isObservable(childrenNodes)) {
79-
childrenNodes
80-
.pipe(takeUntil(this._destroyed))
81-
.subscribe(result => this.updateChildrenNodes(result));
82-
}
83-
this.nodeOutlet.changes
75+
}
76+
const childrenNodes = childrenAccessor?.(this.data);
77+
if (Array.isArray(childrenNodes)) {
78+
this.updateChildrenNodes(childrenNodes as T[]);
79+
} else if (isObservable(childrenNodes)) {
80+
childrenNodes
8481
.pipe(takeUntil(this._destroyed))
8582
.subscribe(() => this.updateChildrenNodes());
8683
}

src/cdk/tree/padding.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ export class CdkTreeNodePadding<T, K = T> implements OnDestroy {
8181
/** The padding indent value for the tree node. Returns a string with px numbers if not null. */
8282
_paddingIndent(): string | null {
8383
const nodeLevel =
84-
this._treeNode.data && this._tree.treeControl?.getLevel
85-
? this._tree.treeControl.getLevel(this._treeNode.data)
86-
: null;
84+
(this._treeNode.data && this._tree._getLevelAccessor()?.(this._treeNode.data)) ?? null;
8785
const level = this._level == null ? nodeLevel : this._level;
8886
return typeof level === 'number' ? `${level * this._indent}${this.indentUnits}` : null;
8987
}

src/cdk/tree/toggle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export class CdkTreeNodeToggle<T, K = T> {
3535

3636
_toggle(event: Event): void {
3737
this.recursive
38-
? this._tree.treeControl?.toggleDescendants(this._treeNode.data)
39-
: this._tree.treeControl?.toggle(this._treeNode.data);
38+
? this._tree.toggleDescendants(this._treeNode.data)
39+
: this._tree.toggle(this._treeNode.data);
4040

4141
event.stopPropagation();
4242
}

src/cdk/tree/tree.ts

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,34 @@ import {
2929
} from '@angular/core';
3030
import {
3131
BehaviorSubject,
32+
concat,
3233
isObservable,
34+
merge,
3335
Observable,
3436
of as observableOf,
3537
Subject,
3638
Subscription,
3739
} from 'rxjs';
38-
import {takeUntil} from 'rxjs/operators';
40+
import {reduce, switchMap, take, takeUntil} from 'rxjs/operators';
3941
import {TreeControl} from './control/tree-control';
4042
import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node';
4143
import {CdkTreeNodeOutlet} from './outlet';
4244
import {
4345
getMultipleTreeControlsError,
46+
getTreeControlFunctionsMissingError,
4447
getTreeControlMissingError,
4548
getTreeMissingMatchingNodeDefError,
4649
getTreeMultipleDefaultNodeDefsError,
4750
getTreeNoValidDataSourceError,
4851
} from './tree-errors';
49-
import {coerceNumberProperty} from '@angular/cdk/coercion';
52+
import {BooleanInput, coerceNumberProperty} from '@angular/cdk/coercion';
53+
54+
function coerceObservable<T>(data: T | Observable<T>): Observable<T> {
55+
if (!isObservable(data)) {
56+
return observableOf(data);
57+
}
58+
return data;
59+
}
5060

5161
/**
5262
* CDK tree component that connects with a data source to retrieve data of type `T` and renders
@@ -357,52 +367,111 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
357367

358368
/** Whether the data node is expanded or collapsed. Returns true if it's expanded. */
359369
isExpanded(dataNode: T): boolean {
360-
throw new Error('not implemented');
370+
return (
371+
this.treeControl?.isExpanded(dataNode) ??
372+
this._expansionModel?.isSelected(this._trackExpansionKey(dataNode)) ??
373+
false
374+
);
361375
}
362376

363377
/** If the data node is currently expanded, collapse it. Otherwise, expand it. */
364378
toggle(dataNode: T): void {
365-
throw new Error('not implemented');
379+
if (this.treeControl) {
380+
this.treeControl.toggle(dataNode);
381+
} else if (this._expansionModel) {
382+
this._expansionModel.toggle(this._trackExpansionKey(dataNode));
383+
}
366384
}
367385

368386
/** Expand the data node. If it is already expanded, does nothing. */
369387
expand(dataNode: T): void {
370-
throw new Error('not implemented');
388+
if (this.treeControl) {
389+
this.treeControl.expand(dataNode);
390+
} else if (this._expansionModel) {
391+
this._expansionModel.select(this._trackExpansionKey(dataNode));
392+
}
371393
}
372394

373395
/** Collapse the data node. If it is already collapsed, does nothing. */
374396
collapse(dataNode: T): void {
375-
throw new Error('not implemented');
397+
if (this.treeControl) {
398+
this.treeControl.collapse(dataNode);
399+
} else if (this._expansionModel) {
400+
this._expansionModel.deselect(this._trackExpansionKey(dataNode));
401+
}
376402
}
377403

378404
/**
379405
* If the data node is currently expanded, collapse it and all its descendants.
380406
* Otherwise, expand it and all its descendants.
381407
*/
382408
toggleDescendants(dataNode: T): void {
383-
throw new Error('not implemented');
409+
if (this.treeControl) {
410+
this.treeControl.toggleDescendants(dataNode);
411+
} else if (this._expansionModel) {
412+
if (this.isExpanded(dataNode)) {
413+
this.collapseDescendants(dataNode);
414+
} else {
415+
this.expandDescendants(dataNode);
416+
}
417+
}
384418
}
385419

386420
/**
387-
* Expand the data node and all its descendants. If they are already expanded, does nothing.
388-
*/
421+
* Expand the data node and all its descendants. If they are already expanded, does nothing. */
389422
expandDescendants(dataNode: T): void {
390-
throw new Error('not implemented');
423+
if (this.treeControl) {
424+
this.treeControl.expandDescendants(dataNode);
425+
} else if (this._expansionModel) {
426+
const expansionModel = this._expansionModel;
427+
this._getDescendants(dataNode)
428+
.pipe(takeUntil(this._onDestroy))
429+
.subscribe(children => {
430+
expansionModel.select(...children.map(child => this._trackExpansionKey(child)));
431+
});
432+
}
391433
}
392434

393435
/** Collapse the data node and all its descendants. If it is already collapsed, does nothing. */
394436
collapseDescendants(dataNode: T): void {
395-
throw new Error('not implemented');
437+
if (this.treeControl) {
438+
this.treeControl.collapseDescendants(dataNode);
439+
} else if (this._expansionModel) {
440+
const expansionModel = this._expansionModel;
441+
this._getDescendants(dataNode)
442+
.pipe(takeUntil(this._onDestroy))
443+
.subscribe(children => {
444+
expansionModel.deselect(...children.map(child => this._trackExpansionKey(child)));
445+
});
446+
}
396447
}
397448

398449
/** Expands all data nodes in the tree. */
399450
expandAll(): void {
400-
throw new Error('not implemented');
451+
if (this.treeControl) {
452+
this.treeControl.expandAll();
453+
} else if (this._expansionModel) {
454+
const expansionModel = this._expansionModel;
455+
this._getAllDescendants()
456+
.pipe(takeUntil(this._onDestroy))
457+
.subscribe(children => {
458+
expansionModel.select(...children.map(child => this._trackExpansionKey(child)));
459+
});
460+
}
401461
}
402462

403463
/** Collapse all data nodes in the tree. */
404464
collapseAll(): void {
405-
throw new Error('not implemented');
465+
if (this.treeControl) {
466+
this.treeControl.collapseAll();
467+
} else if (this._expansionModel) {
468+
const expansionModel = this._expansionModel;
469+
this._getAllDescendants()
470+
.pipe(takeUntil(this._onDestroy))
471+
.subscribe(children => {
472+
expansionModel.deselect(...children.map(child => this._trackExpansionKey(child)));
473+
});
474+
}
406475
}
407476

408477
/** Level accessor, used for compatibility between the old Tree and new Tree */
@@ -414,6 +483,72 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
414483
_getChildrenAccessor() {
415484
return this.treeControl?.getChildren ?? this.childrenAccessor;
416485
}
486+
487+
private _getAllDescendants(): Observable<T[]> {
488+
return merge(...(this._dataNodes?.map(dataNode => this._getDescendants(dataNode)) ?? []));
489+
}
490+
491+
private _getDescendants(dataNode: T): Observable<T[]> {
492+
if (this.treeControl) {
493+
return observableOf(this.treeControl.getDescendants(dataNode));
494+
}
495+
if (this.levelAccessor) {
496+
// If we have no known nodes, we wouldn't be able to determine descendants
497+
if (!this._dataNodes) {
498+
return observableOf([]);
499+
}
500+
const startIndex = this._dataNodes.indexOf(dataNode);
501+
const results: T[] = [dataNode];
502+
503+
// Goes through flattened tree nodes in the `dataNodes` array, and get all descendants.
504+
// The level of descendants of a tree node must be greater than the level of the given
505+
// tree node.
506+
// If we reach a node whose level is equal to the level of the tree node, we hit a sibling.
507+
// If we reach a node whose level is greater than the level of the tree node, we hit a
508+
// sibling of an ancestor.
509+
const currentLevel = this.levelAccessor(dataNode);
510+
for (
511+
let i = startIndex + 1;
512+
i < this._dataNodes.length && currentLevel < this.levelAccessor(this._dataNodes[i]);
513+
i++
514+
) {
515+
results.push(this._dataNodes[i]);
516+
}
517+
return observableOf(results);
518+
}
519+
if (this.childrenAccessor) {
520+
return this._getChildrenRecursively(dataNode).pipe(
521+
reduce(
522+
(memo: T[], next) => {
523+
memo.push(...next);
524+
return memo;
525+
},
526+
[dataNode],
527+
),
528+
);
529+
}
530+
throw getTreeControlMissingError();
531+
}
532+
533+
private _getChildrenRecursively(dataNode: T): Observable<T[]> {
534+
if (!this.childrenAccessor) {
535+
return observableOf([]);
536+
}
537+
538+
return coerceObservable(this.childrenAccessor(dataNode)).pipe(
539+
take(1),
540+
switchMap(children => {
541+
return concat(
542+
observableOf(children),
543+
...children.map(child => this._getChildrenRecursively(child)),
544+
);
545+
}),
546+
);
547+
}
548+
549+
private _trackExpansionKey(dataNode: T): K {
550+
return this.expansionKey?.(dataNode) ?? (dataNode as unknown as K);
551+
}
417552
}
418553

419554
/**

0 commit comments

Comments
 (0)