diff --git a/src/cdk/tree/control/base-tree-control.ts b/src/cdk/tree/control/base-tree-control.ts index 4fad8b20e947..af6af76378f0 100644 --- a/src/cdk/tree/control/base-tree-control.ts +++ b/src/cdk/tree/control/base-tree-control.ts @@ -9,7 +9,12 @@ import {SelectionModel} from '@angular/cdk/collections'; import {Observable} from 'rxjs'; import {TreeControl} from './tree-control'; -/** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */ +/** + * Base tree control. It has basic toggle/expand/collapse operations on a single data node. + * + * @deprecated Use one of levelAccessor or childrenAccessor + * @breaking-change 0.0.0-PLACEHOLDER + */ export abstract class BaseTreeControl implements TreeControl { /** Gets a list of descendent data nodes of a subtree rooted at given data node recursively. */ abstract getDescendants(dataNode: T): T[]; diff --git a/src/cdk/tree/control/flat-tree-control.ts b/src/cdk/tree/control/flat-tree-control.ts index 3c128295f0d4..9e361ce4bdc3 100644 --- a/src/cdk/tree/control/flat-tree-control.ts +++ b/src/cdk/tree/control/flat-tree-control.ts @@ -13,7 +13,12 @@ export interface FlatTreeControlOptions { trackBy?: (dataNode: T) => K; } -/** Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */ +/** + * Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. + * + * @deprecated Use one of levelAccessor or childrenAccessor + * @breaking-change 14.0.0 + */ export class FlatTreeControl extends BaseTreeControl { /** Construct with flat tree data node functions getLevel and isExpandable. */ constructor( diff --git a/src/cdk/tree/control/nested-tree-control.ts b/src/cdk/tree/control/nested-tree-control.ts index 6a30fabcfbdd..d8e8009b3115 100644 --- a/src/cdk/tree/control/nested-tree-control.ts +++ b/src/cdk/tree/control/nested-tree-control.ts @@ -14,7 +14,12 @@ export interface NestedTreeControlOptions { trackBy?: (dataNode: T) => K; } -/** Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type. */ +/** + * Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type. + * + * @deprecated Use one of levelAccessor or childrenAccessor + * @breaking-change 14.0.0 + */ export class NestedTreeControl extends BaseTreeControl { /** Construct with nested tree function getChildren. */ constructor( diff --git a/src/cdk/tree/control/tree-control.ts b/src/cdk/tree/control/tree-control.ts index f32e0f4c5852..99e82ac2f712 100644 --- a/src/cdk/tree/control/tree-control.ts +++ b/src/cdk/tree/control/tree-control.ts @@ -12,6 +12,9 @@ import {Observable} from 'rxjs'; * Tree control interface. User can implement TreeControl to expand/collapse dataNodes in the tree. * The CDKTree will use this TreeControl to expand/collapse a node. * User can also use it outside the `` to control the expansion status of the tree. + * + * @deprecated Use one of levelAccessor or childrenAccessor + * @breaking-change 14.0.0 */ export interface TreeControl { /** The saved tree nodes data for `expandAll` action. */ diff --git a/src/cdk/tree/nested-node.ts b/src/cdk/tree/nested-node.ts index 55c05c875167..91a328744e42 100644 --- a/src/cdk/tree/nested-node.ts +++ b/src/cdk/tree/nested-node.ts @@ -16,12 +16,10 @@ import { OnInit, QueryList, } from '@angular/core'; -import {isObservable} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from './outlet'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; /** * Nested node is a child of ``. It works with nested tree. @@ -69,22 +67,13 @@ export class CdkNestedTreeNode ngAfterContentInit() { this._dataDiffer = this._differs.find([]).create(this._tree.trackBy); - const childrenAccessor = this._tree._getChildrenAccessor(); - if (!childrenAccessor && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throw getTreeControlFunctionsMissingError(); - } else if (childrenAccessor) { - const childrenNodes = childrenAccessor(this.data); - if (Array.isArray(childrenNodes)) { - this.updateChildrenNodes(childrenNodes as T[]); - } else if (isObservable(childrenNodes)) { - childrenNodes - .pipe(takeUntil(this._destroyed)) - .subscribe(result => this.updateChildrenNodes(result)); - } - this.nodeOutlet.changes - .pipe(takeUntil(this._destroyed)) - .subscribe(() => this.updateChildrenNodes()); - } + this._tree + ._getDirectChildren(this.data) + .pipe(takeUntil(this._destroyed)) + .subscribe(result => this.updateChildrenNodes(result)); + this.nodeOutlet.changes + .pipe(takeUntil(this._destroyed)) + .subscribe(() => this.updateChildrenNodes()); } // This is a workaround for https://github.com/angular/angular/issues/23091 @@ -106,7 +95,7 @@ export class CdkNestedTreeNode } if (outlet && this._children) { const viewContainer = outlet.viewContainer; - this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data); + this._tree._renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data); } else { // Reset the data differ if there's no children nodes displayed this._dataDiffer.diff([]); diff --git a/src/cdk/tree/padding.ts b/src/cdk/tree/padding.ts index 1953550bf0d3..54c5c51a0774 100644 --- a/src/cdk/tree/padding.ts +++ b/src/cdk/tree/padding.ts @@ -80,8 +80,7 @@ export class CdkTreeNodePadding implements OnDestroy { /** The padding indent value for the tree node. Returns a string with px numbers if not null. */ _paddingIndent(): string | null { - const nodeLevel = - (this._treeNode.data && this._tree._getLevelAccessor()?.(this._treeNode.data)) ?? null; + const nodeLevel = (this._treeNode.data && this._tree._getLevel(this._treeNode.data)) ?? null; const level = this._level == null ? nodeLevel : this._level; return typeof level === 'number' ? `${level * this._indent}${this.indentUnits}` : null; } diff --git a/src/cdk/tree/toggle.ts b/src/cdk/tree/toggle.ts index e1a4c6938e92..a5f95cee5fab 100644 --- a/src/cdk/tree/toggle.ts +++ b/src/cdk/tree/toggle.ts @@ -18,6 +18,7 @@ import {CdkTree, CdkTreeNode} from './tree'; selector: '[cdkTreeNodeToggle]', host: { '(click)': '_toggle($event)', + 'tabindex': '0', }, }) export class CdkTreeNodeToggle { diff --git a/src/cdk/tree/tree-errors.ts b/src/cdk/tree/tree-errors.ts index bb5ae840a31a..d07879aa5e69 100644 --- a/src/cdk/tree/tree-errors.ts +++ b/src/cdk/tree/tree-errors.ts @@ -48,9 +48,9 @@ export function getMultipleTreeControlsError() { } /** - * Returns an error to be thrown when tree control did not implement functions for flat/nested node. + * Returns an error to be thrown when the node type is not specified. * @docs-private */ -export function getTreeControlFunctionsMissingError() { - return Error(`Could not find functions for nested/flat tree in tree control.`); +export function getTreeControlNodeTypeUnspecifiedError() { + return Error(`The nodeType was not specified for the tree.`); } diff --git a/src/cdk/tree/tree-redesign.spec.ts b/src/cdk/tree/tree-redesign.spec.ts index 644d6ed03853..109d9b32a353 100644 --- a/src/cdk/tree/tree-redesign.spec.ts +++ b/src/cdk/tree/tree-redesign.spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import { Component, ErrorHandler, @@ -24,7 +24,6 @@ import {map} from 'rxjs/operators'; import {CdkTreeModule, CdkTreeNodePadding} from './index'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; /** * This is a cloned version of `tree.spec.ts` that contains all the same tests, @@ -1127,20 +1126,6 @@ describe('CdkTree redesign', () => { expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); }); }); - - it('should throw an error when missing function in nested tree', fakeAsync(() => { - configureCdkTreeTestingModule([NestedCdkErrorTreeApp]); - expect(() => { - try { - TestBed.createComponent(NestedCdkErrorTreeApp).detectChanges(); - flush(); - } catch { - flush(); - } finally { - flush(); - } - }).toThrowError(getTreeControlFunctionsMissingError().message); - })); }); describe('with depth', () => { @@ -1356,7 +1341,8 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { @Component({ template: ` - + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1417,7 +1405,8 @@ class NestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1445,7 +1434,8 @@ class StaticNestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1469,7 +1459,8 @@ class WhenNodeNestedCdkTreeApp { @Component({ template: ` - + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1515,7 +1507,8 @@ class NestedCdkTreeAppWithToggle { @Component({ template: ` - + + + + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1615,7 +1611,8 @@ class ArrayDataSourceNestedCdkTreeApp { @Component({ template: ` - + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1637,28 +1634,8 @@ class ObservableDataSourceNestedCdkTreeApp { @Component({ template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - `, -}) -class NestedCdkErrorTreeApp { - getLevel = (node: TestData) => node.level; - - isExpandable = (node: TestData) => node.children.length > 0; - - dataSource: FakeDataSource | null = new FakeDataSource(); - - @ViewChild(CdkTree) tree: CdkTree; -} - -@Component({ - template: ` - + {{level}} [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1681,7 +1658,8 @@ class DepthNestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1712,7 +1690,8 @@ class CdkTreeAppWithTrackBy { @Component({ template: ` - + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 8c58ee0db1c8..3ba2ca114050 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import { Component, ErrorHandler, @@ -27,7 +27,6 @@ import {FlatTreeControl} from './control/flat-tree-control'; import {NestedTreeControl} from './control/nested-tree-control'; import {CdkTreeModule, CdkTreeNodePadding} from './index'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; describe('CdkTree', () => { /** Represents an indent for expectNestedTreeToMatch */ @@ -1126,20 +1125,6 @@ describe('CdkTree', () => { expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); }); }); - - it('should throw an error when missing function in nested tree', fakeAsync(() => { - configureCdkTreeTestingModule([NestedCdkErrorTreeApp]); - expect(() => { - try { - TestBed.createComponent(NestedCdkErrorTreeApp).detectChanges(); - flush(); - } catch { - flush(); - } finally { - flush(); - } - }).toThrowError(getTreeControlFunctionsMissingError().message); - })); }); describe('with depth', () => { @@ -1633,28 +1618,6 @@ class ObservableDataSourceNestedCdkTreeApp { @ViewChild(CdkTree) tree: CdkTree; } -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - `, -}) -class NestedCdkErrorTreeApp { - getLevel = (node: TestData) => node.level; - - isExpandable = (node: TestData) => node.children.length > 0; - - treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); - - @ViewChild(CdkTree) tree: CdkTree; -} - @Component({ template: ` diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 420826d6f504..780e8697ad1b 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {FocusableOption} from '@angular/cdk/a11y'; +import {coerceNumberProperty} from '@angular/cdk/coercion'; import {CollectionViewer, DataSource, isDataSource, SelectionModel} from '@angular/cdk/collections'; import { AfterContentChecked, @@ -29,7 +30,9 @@ import { } from '@angular/core'; import { BehaviorSubject, + combineLatest, concat, + EMPTY, isObservable, merge, Observable, @@ -37,18 +40,18 @@ import { Subject, Subscription, } from 'rxjs'; -import {reduce, switchMap, take, takeUntil} from 'rxjs/operators'; +import {concatMap, map, reduce, startWith, switchMap, take, takeUntil, tap} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; import { getMultipleTreeControlsError, getTreeControlMissingError, + getTreeControlNodeTypeUnspecifiedError, getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError, } from './tree-errors'; -import {coerceNumberProperty} from '@angular/cdk/coercion'; function coerceObservable(data: T | Observable): Observable { if (!isObservable(data)) { @@ -91,7 +94,21 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, private _dataSubscription: Subscription | null; /** Level of nodes */ - private _levels: Map = new Map(); + private _levels: Map = new Map(); + + /** The immediate parents for a node. This is `null` if there is no parent. */ + private _parents: Map = new Map(); + + /** + * The internal node groupings for each node; we use this, primarily for flattened trees, to + * determine where a particular node is within each group. + * + * The structure of this is that: + * - the outer index is the level + * - the inner index is the parent node for this particular group. If there is no parent node, we + * use `null`. + */ + private _groups: Map> = new Map>(); /** * Provides a stream containing the latest data array to render. Influenced by the tree's @@ -146,6 +163,16 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, */ @Input() expansionKey?: (dataNode: T) => K; + /** + * What type of node is being used in the tree. This must be provided if either of + * `levelAccessor` or `childrenAccessor` are provided. + * + * This controls what selection of data the tree will render. + */ + // NB: we're unable to determine this ourselves; Angular's ContentChildren + // unfortunately does not pick up the necessary information. + @Input() nodeType?: 'flat' | 'nested'; + // Outlets within the tree's template where the dataNodes will be inserted. @ViewChild(CdkTreeNodeOutlet, {static: true}) _nodeOutlet: CdkTreeNodeOutlet; @@ -170,8 +197,18 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, /** Keep track of which nodes are expanded. */ private _expansionModel?: SelectionModel; - /** Maintain a synchronous cache of the currently known data nodes. */ - private _dataNodes?: readonly T[]; + + /** + * Maintain a synchronous cache of the currently known data nodes. In the + * case of nested nodes (i.e. if `nodeType` is 'nested'), this will + * only contain the root nodes. + */ + private _dataNodes: BehaviorSubject = new BehaviorSubject([]); + + /** The mapping between data and the node that is rendered. */ + private _nodes: BehaviorSubject>> = new BehaviorSubject( + new Map>(), + ); constructor(private _differs: IterableDiffers, private _changeDetectorRef: ChangeDetectorRef) {} @@ -186,6 +223,11 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, } else if (provided === 0) { throw getTreeControlMissingError(); } + + // Check that the node type is also provided if treeControl is not. + if (!this.treeControl && !this.nodeType) { + throw getTreeControlNodeTypeUnspecifiedError(); + } } if (!this.treeControl) { @@ -265,21 +307,23 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, if (dataStream) { this._dataSubscription = dataStream - .pipe(takeUntil(this._onDestroy)) - .subscribe(data => this.renderNodeChanges(data)); + .pipe( + switchMap(data => this._convertChildren(data)), + takeUntil(this._onDestroy), + ) + .subscribe(data => this._renderNodeChanges(data)); } else if (typeof ngDevMode === 'undefined' || ngDevMode) { throw getTreeNoValidDataSourceError(); } } /** Check for changes made in the data and render each change (node added/removed/moved). */ - renderNodeChanges( + _renderNodeChanges( data: readonly T[], dataDiffer: IterableDiffer = this._dataDiffer, viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer, parentData?: T, ) { - this._dataNodes = data; const changes = dataDiffer.diff(data); if (!changes) { return; @@ -295,7 +339,10 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, this.insertNode(data[currentIndex!], currentIndex!, viewContainer, parentData); } else if (currentIndex == null) { viewContainer.remove(adjustedPreviousIndex!); - this._levels.delete(item.item); + const group = this._getNodeGroup(item.item); + this._levels.delete(this._getExpansionKey(item.item)); + this._parents.delete(this._getExpansionKey(item.item)); + group.splice(group.indexOf(item.item), 1); } else { const view = viewContainer.get(adjustedPreviousIndex!); viewContainer.move(view!, currentIndex); @@ -337,17 +384,34 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // Node context that will be provided to created embedded view const context = new CdkTreeNodeOutletContext(nodeData); + parentData ??= this._parents.get(this._getExpansionKey(nodeData)) ?? undefined; // If the tree is flat tree, then use the `getLevel` function in flat tree control // Otherwise, use the level of parent node. const levelAccessor = this._getLevelAccessor(); if (levelAccessor) { context.level = levelAccessor(nodeData); - } else if (typeof parentData !== 'undefined' && this._levels.has(parentData)) { - context.level = this._levels.get(parentData)! + 1; + } else if ( + typeof parentData !== 'undefined' && + this._levels.has(this._getExpansionKey(parentData)) + ) { + context.level = this._levels.get(this._getExpansionKey(parentData))! + 1; } else { context.level = 0; } - this._levels.set(nodeData, context.level); + this._levels.set(this._getExpansionKey(nodeData), context.level); + const parent = parentData ?? this._findParentForNode(nodeData, index); + this._parents.set(this._getExpansionKey(nodeData), parent); + + // Determine where to insert this new node into the group, then insert it. + // We do this by looking at the previous node in our flattened node list. If it's in the same + // group, we place the current node after. Otherwise, we place it at the start of the group. + const currentGroup = this._groups.get(context.level) ?? new Map(); + const group = currentGroup.get(parent) ?? []; + const previousNode = this._dataNodes.value?.[index - 1]; + const groupInsertionIndex = (previousNode && group.indexOf(previousNode) + 1) ?? 0; + group.splice(groupInsertionIndex, 0, nodeData); + currentGroup.set(parent, group); + this._groups.set(context.level, currentGroup); // Use default tree nodeOutlet, or nested node's nodeOutlet const container = viewContainer ? viewContainer : this._nodeOutlet.viewContainer; @@ -481,6 +545,106 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return this.treeControl?.getChildren ?? this.childrenAccessor; } + /** + * Gets the direct children of a node; used for compatibility between the old tree and the + * new tree. + */ + _getDirectChildren(dataNode: T): Observable { + const levelAccessor = this._getLevelAccessor(); + if (levelAccessor && this._expansionModel) { + const key = this._getExpansionKey(dataNode); + const isExpanded = this._expansionModel.changed.pipe( + switchMap(changes => { + if (changes.added.includes(key)) { + return observableOf(true); + } else if (changes.removed.includes(key)) { + return observableOf(false); + } + return EMPTY; + }), + startWith(this.isExpanded(dataNode)), + ); + + return combineLatest([isExpanded, this._dataNodes]).pipe( + map(([expanded, dataNodes]) => { + if (!expanded) { + return []; + } + const startIndex = dataNodes.indexOf(dataNode); + const level = levelAccessor(dataNode) + 1; + const results: T[] = []; + + // Goes through flattened tree nodes in the `dataNodes` array, and get all direct descendants. + // The level of descendants of a tree node must be equal to the level of the given + // tree node + 1. + // If we reach a node whose level is equal to the level of the tree node, we hit a sibling. + // If we reach a node whose level is greater than the level of the tree node, we hit a + // sibling of an ancestor. + for (let i = startIndex + 1; i < dataNodes.length; i++) { + const currentLevel = levelAccessor(dataNodes[i]); + if (level > currentLevel) { + break; + } + if (level === currentLevel) { + results.push(dataNodes[i]); + } + } + return results; + }), + ); + } + const childrenAccessor = this._getChildrenAccessor(); + if (childrenAccessor) { + return coerceObservable(childrenAccessor(dataNode) ?? []); + } + throw getTreeControlMissingError(); + } + + /** + * Adds the specified node component to the tree's internal registry. + * + * This primarily facilitates keyboard navigation. + */ + _registerNode(node: CdkTreeNode) { + this._nodes.value.set(this._getExpansionKey(node.data), node); + this._nodes.next(this._nodes.value); + } + + /** Removes the specified node component from the tree's internal registry. */ + _unregisterNode(node: CdkTreeNode) { + this._nodes.value.delete(this._getExpansionKey(node.data)); + this._nodes.next(this._nodes.value); + } + + /** + * For the given node, determine the level where this node appears in the tree. + * + * This is intended to be used for `aria-level` but is 0-indexed. + */ + _getLevel(node: T) { + return this._levels.get(this._getExpansionKey(node)); + } + + /** + * For the given node, determine the size of the parent's child set. + * + * This is intended to be used for `aria-setsize`. + */ + _getSetSize(dataNode: T) { + const group = this._getNodeGroup(dataNode); + return group.length; + } + + /** + * For the given node, determine the index (starting from 1) of the node in its parent's child set. + * + * This is intended to be used for `aria-posinset`. + */ + _getPositionInSet(dataNode: T) { + const group = this._getNodeGroup(dataNode); + return group.indexOf(dataNode) + 1; + } + /** * Gets all nodes in the tree, through recursive expansion. * @@ -493,7 +657,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * the tree. */ private _getAllDescendants(): Observable { - return merge(...(this._dataNodes?.map(dataNode => this._getDescendants(dataNode)) ?? [])); + return merge(...this._dataNodes.value.map(dataNode => this._getDescendants(dataNode))); } private _getDescendants(dataNode: T): Observable { @@ -501,11 +665,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return observableOf(this.treeControl.getDescendants(dataNode)); } if (this.levelAccessor) { - // If we have no known nodes, we wouldn't be able to determine descendants - if (!this._dataNodes) { - return observableOf([]); - } - const startIndex = this._dataNodes.indexOf(dataNode); + const startIndex = this._dataNodes.value.indexOf(dataNode); const results: T[] = [dataNode]; // Goes through flattened tree nodes in the `dataNodes` array, and get all descendants. @@ -517,15 +677,16 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, const currentLevel = this.levelAccessor(dataNode); for ( let i = startIndex + 1; - i < this._dataNodes.length && currentLevel < this.levelAccessor(this._dataNodes[i]); + i < this._dataNodes.value.length && + currentLevel < this.levelAccessor(this._dataNodes.value[i]); i++ ) { - results.push(this._dataNodes[i]); + results.push(this._dataNodes.value[i]); } return observableOf(results); } if (this.childrenAccessor) { - return this._getChildrenRecursively(dataNode).pipe( + return this._getAllChildrenRecursively(dataNode).pipe( reduce( (allChildren: T[], nextChildren) => { allChildren.push(...nextChildren); @@ -544,7 +705,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * This will emit multiple times, in the order that the children will appear * in the tree, and can be combined with a `reduce` operator. */ - private _getChildrenRecursively(dataNode: T): Observable { + private _getAllChildrenRecursively(dataNode: T): Observable { if (!this.childrenAccessor) { return observableOf([]); } @@ -552,9 +713,12 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return coerceObservable(this.childrenAccessor(dataNode)).pipe( take(1), switchMap(children => { - return concat( - observableOf(children), - ...children.map(child => this._getChildrenRecursively(child)), + // Here, we cache the parents of a particular child so that we can compute the levels. + for (const child of children) { + this._parents.set(this._getExpansionKey(child), dataNode); + } + return observableOf(...children).pipe( + concatMap(child => concat(observableOf([child]), this._getAllChildrenRecursively(child))), ); }), ); @@ -570,6 +734,65 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // - if it's not, then K will be defaulted to T. return this.expansionKey?.(dataNode) ?? (dataNode as unknown as K); } + + /** + * Converts children for certain tree configurations. Note also that this + * caches the known nodes for use in other parts of the tree. + */ + private _convertChildren(nodes: readonly T[]): Observable { + // The only situations where we have to convert children types is when + // they're mismatched; i.e. if the tree is using a childrenAccessor and the + // nodes are flat, or if the tree is using a levelAccessor and the nodes are + // nested. + if (this.childrenAccessor && this.nodeType === 'flat') { + // This flattens children into a single array. + return observableOf(...nodes).pipe( + concatMap(node => concat(observableOf([node]), this._getAllChildrenRecursively(node))), + reduce((results, children) => { + results.push(...children); + return results; + }, [] as T[]), + tap(allNodes => { + this._dataNodes.next(allNodes); + }), + ); + } else if (this.levelAccessor && this.nodeType === 'nested') { + this._dataNodes.next(nodes); + // In the nested case, we only look for root nodes. The CdkNestedNode + // itself will handle rendering each individual node's children. + const levelAccessor = this.levelAccessor; + return observableOf(nodes.filter(node => levelAccessor(node) === 0)); + } else { + this._dataNodes.next(nodes); + return observableOf(nodes); + } + } + + private _getNodeGroup(node: T) { + const level = this._levels.get(this._getExpansionKey(node)); + const parent = this._parents.get(this._getExpansionKey(node)); + const group = this._groups.get(level ?? 0)?.get(parent ?? null); + return group ?? [node]; + } + + private _findParentForNode(node: T, index: number) { + // In all cases, we have a mapping from node to level; all we need to do here is backtrack in + // our flattened list of nodes to determine the first node that's of a level lower than the + // provided node. + if (!this._dataNodes) { + return null; + } + const currentLevel = this._levels.get(this._getExpansionKey(node)) ?? 0; + for (let parentIndex = index; parentIndex >= 0; parentIndex--) { + const parentNode = this._dataNodes.value[parentIndex]; + const parentLevel = this._levels.get(this._getExpansionKey(parentNode)) ?? 0; + + if (parentLevel < currentLevel) { + return parentNode; + } + } + return null; + } } /** @@ -581,6 +804,9 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, host: { 'class': 'cdk-tree-node', '[attr.aria-expanded]': 'isExpanded', + '[attr.aria-level]': 'level + 1', + '[attr.aria-posinset]': '_getPositionInSet()', + '[attr.aria-setsize]': '_getSetSize()', }, }) export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit { @@ -645,7 +871,25 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit // If the tree has a levelAccessor, use it to get the level. Otherwise read the // aria-level off the parent node and use it as the level for this node (note aria-level is // 1-indexed, while this property is 0-indexed, so we don't need to increment). - return this._tree._getLevelAccessor()?.(this._data) ?? this._parentNodeAriaLevel; + return this._tree._getLevel(this._data) ?? this._parentNodeAriaLevel; + } + + /** + * Determines the size of this node's parent's child set. + * + * This is intended to be used for `aria-setsize`. + */ + _getSetSize(): number { + return this._tree._getSetSize(this._data); + } + + /** + * Determines the index (starting from 1) of this node in its parent's child set. + * + * This is intended to be used for `aria-posinset`. + */ + _getPositionInSet(): number { + return this._tree._getPositionInSet(this._data); } constructor(protected _elementRef: ElementRef, protected _tree: CdkTree) { @@ -655,7 +899,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit ngOnInit(): void { this._parentNodeAriaLevel = getParentNodeAriaLevel(this._elementRef.nativeElement); - this._elementRef.nativeElement.setAttribute('aria-level', `${this.level + 1}`); + this._tree._registerNode(this); } ngOnDestroy() { diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html new file mode 100644 index 000000000000..6976db2de744 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html @@ -0,0 +1,22 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts new file mode 100644 index 000000000000..48ed2152d27a --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts @@ -0,0 +1,57 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {CdkTree} from '@angular/cdk/tree'; +import {Component, ViewChild} from '@angular/core'; +import {timer} from 'rxjs'; +import {mapTo} from 'rxjs/operators'; +import {NestedFoodNode, NESTED_DATA} from '../tree-data'; + +function flattenNodes(nodes: NestedFoodNode[]): NestedFoodNode[] { + const flattenedNodes = []; + for (const node of nodes) { + flattenedNodes.push(node); + if (node.children) { + flattenedNodes.push(...flattenNodes(node.children)); + } + } + return flattenedNodes; +} + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-children-accessor-example', + templateUrl: 'cdk-tree-flat-children-accessor-example.html', + styleUrls: ['cdk-tree-flat-children-accessor-example.css'], +}) +export class CdkTreeFlatChildrenAccessorExample { + @ViewChild(CdkTree) + tree!: CdkTree; + + childrenAccessor = (dataNode: NestedFoodNode) => timer(100).pipe(mapTo(dataNode.children ?? [])); + + dataSource = new ArrayDataSource(NESTED_DATA); + + hasChild = (_: number, node: NestedFoodNode) => !!node.children?.length; + + getParentNode(node: NestedFoodNode) { + for (const parent of flattenNodes(NESTED_DATA)) { + if (parent.children?.includes(node)) { + return parent; + } + } + + return null; + } + + shouldRender(node: NestedFoodNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!this.tree.isExpanded(parent)) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html new file mode 100644 index 000000000000..207aae8a6f82 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html @@ -0,0 +1,24 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts new file mode 100644 index 000000000000..3484953f7abb --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -0,0 +1,43 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {CdkTree} from '@angular/cdk/tree'; +import {Component, ViewChild} from '@angular/core'; +import {FlatFoodNode, FLAT_DATA} from '../tree-data'; + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-level-accessor-example', + templateUrl: 'cdk-tree-flat-level-accessor-example.html', + styleUrls: ['cdk-tree-flat-level-accessor-example.css'], +}) +export class CdkTreeFlatLevelAccessorExample { + @ViewChild(CdkTree) + tree: CdkTree; + + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; + + dataSource = new ArrayDataSource(FLAT_DATA); + + hasChild = (_: number, node: FlatFoodNode) => node.expandable; + + getParentNode(node: FlatFoodNode) { + const nodeIndex = FLAT_DATA.indexOf(node); + + // Determine the node's parent by finding the first preceding node that's + // one level shallower. + for (let i = nodeIndex - 1; i >= 0; i--) { + if (FLAT_DATA[i].level === node.level - 1) { + return FLAT_DATA[i]; + } + } + + return null; + } + + shouldRender(node: FlatFoodNode): boolean { + // This node should render if it is a root node or if all of its ancestors are expanded. + const parent = this.getParentNode(node); + return !parent || (!!this.tree?.isExpanded(parent) && this.shouldRender(parent)); + } +} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css new file mode 100644 index 000000000000..988fa23745aa --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css @@ -0,0 +1,18 @@ +.example-tree-invisible { + display: none; +} + +.example-tree ul, +.example-tree li { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} +.example-tree-node { + display: block; + line-height: 40px; +} + +.example-tree-node .example-tree-node { + padding-left: 40px; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html new file mode 100644 index 000000000000..71eef12b5f90 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html @@ -0,0 +1,22 @@ + + + + {{node.name}} + + + + + {{node.name}} +
+ +
+
+
diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts new file mode 100644 index 000000000000..3a0ea4dd90b4 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts @@ -0,0 +1,19 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {Component} from '@angular/core'; +import {FlatFoodNode, FLAT_DATA} from '../tree-data'; + +/** + * @title Tree with nested nodes + */ +@Component({ + selector: 'cdk-tree-nested-level-accessor-example', + templateUrl: 'cdk-tree-nested-level-accessor-example.html', + styleUrls: ['cdk-tree-nested-level-accessor-example.css'], +}) +export class CdkTreeNestedLevelAccessorExample { + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; + + dataSource = new ArrayDataSource(FLAT_DATA); + + hasChild = (_: number, node: FlatFoodNode) => node.expandable; +} diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index a7f2305695de..fc8afe4a305d 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -2,12 +2,27 @@ import {CdkTreeModule} from '@angular/cdk/tree'; import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; +import {CdkTreeFlatChildrenAccessorExample} from './cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example'; +import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example'; import {CdkTreeFlatExample} from './cdk-tree-flat/cdk-tree-flat-example'; +import {CdkTreeNestedLevelAccessorExample} from './cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example'; import {CdkTreeNestedExample} from './cdk-tree-nested/cdk-tree-nested-example'; -export {CdkTreeFlatExample, CdkTreeNestedExample}; +export { + CdkTreeFlatExample, + CdkTreeNestedExample, + CdkTreeFlatLevelAccessorExample, + CdkTreeFlatChildrenAccessorExample, + CdkTreeNestedLevelAccessorExample, +}; -const EXAMPLES = [CdkTreeFlatExample, CdkTreeNestedExample]; +const EXAMPLES = [ + CdkTreeFlatExample, + CdkTreeNestedExample, + CdkTreeFlatLevelAccessorExample, + CdkTreeFlatChildrenAccessorExample, + CdkTreeNestedLevelAccessorExample, +]; @NgModule({ imports: [CdkTreeModule, MatButtonModule, MatIconModule], diff --git a/src/components-examples/cdk/tree/tree-data.ts b/src/components-examples/cdk/tree/tree-data.ts new file mode 100644 index 000000000000..780c969d532f --- /dev/null +++ b/src/components-examples/cdk/tree/tree-data.ts @@ -0,0 +1,94 @@ +/** Flat node with expandable and level information */ +export interface FlatFoodNode { + expandable: boolean; + name: string; + level: number; + isExpanded?: boolean; +} + +export const FLAT_DATA: FlatFoodNode[] = [ + { + name: 'Fruit', + expandable: true, + level: 0, + }, + { + name: 'Apple', + expandable: false, + level: 1, + }, + { + name: 'Banana', + expandable: false, + level: 1, + }, + { + name: 'Fruit loops', + expandable: false, + level: 1, + }, + { + name: 'Vegetables', + expandable: true, + level: 0, + }, + { + name: 'Green', + expandable: true, + level: 1, + }, + { + name: 'Broccoli', + expandable: false, + level: 2, + }, + { + name: 'Brussels sprouts', + expandable: false, + level: 2, + }, + { + name: 'Orange', + expandable: true, + level: 1, + }, + { + name: 'Pumpkins', + expandable: false, + level: 2, + }, + { + name: 'Carrots', + expandable: false, + level: 2, + }, +]; + +/** + * Food data with nested structure. + * Each node has a name and an optional list of children. + */ +export interface NestedFoodNode { + name: string; + children?: NestedFoodNode[]; +} + +export const NESTED_DATA: NestedFoodNode[] = [ + { + name: 'Fruit', + children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops'}], + }, + { + name: 'Vegetables', + children: [ + { + name: 'Green', + children: [{name: 'Broccoli'}, {name: 'Brussels sprouts'}], + }, + { + name: 'Orange', + children: [{name: 'Pumpkins'}, {name: 'Carrots'}], + }, + ], + }, +]; diff --git a/src/dev-app/tree/tree-demo.html b/src/dev-app/tree/tree-demo.html index 97160db7c1cb..21fb0e5d8196 100644 --- a/src/dev-app/tree/tree-demo.html +++ b/src/dev-app/tree/tree-demo.html @@ -7,6 +7,14 @@ CDK Flat tree + + CDK Flat tree (levelAccessor) + + + + CDK Flat tree (childrenAccessor) + + Nested tree @@ -15,6 +23,10 @@ CDK Nested tree + + CDK Nested tree (levelAccessor) + + Todo list tree diff --git a/tools/public_api_guard/cdk/tree.md b/tools/public_api_guard/cdk/tree.md index 5bfcdd4a9269..9dcd88654133 100644 --- a/tools/public_api_guard/cdk/tree.md +++ b/tools/public_api_guard/cdk/tree.md @@ -29,7 +29,7 @@ import { TemplateRef } from '@angular/core'; import { TrackByFunction } from '@angular/core'; import { ViewContainerRef } from '@angular/core'; -// @public +// @public @deprecated export abstract class BaseTreeControl implements TreeControl { collapse(dataNode: T): void; collapseAll(): void; @@ -70,7 +70,7 @@ export class CdkNestedTreeNode extends CdkTreeNode implements Af nodeOutlet: QueryList; protected updateChildrenNodes(children?: T[]): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-nested-tree-node", ["cdkNestedTreeNode"], { "role": "role"; "disabled": "disabled"; "tabIndex": "tabIndex"; }, {}, ["nodeOutlet"], never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-nested-tree-node", ["cdkNestedTreeNode"], { "role": { "alias": "role"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; }, {}, ["nodeOutlet"], never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -89,8 +89,12 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, expandDescendants(dataNode: T): void; expansionKey?: (dataNode: T) => K; _getChildrenAccessor(): ((dataNode: T) => T[] | Observable | null | undefined) | undefined; + _getDirectChildren(dataNode: T): Observable; + _getLevel(node: T): number | undefined; _getLevelAccessor(): ((dataNode: T) => number) | undefined; _getNodeDef(data: T, i: number): CdkTreeNodeDef; + _getPositionInSet(dataNode: T): number; + _getSetSize(dataNode: T): number; insertNode(nodeData: T, index: number, viewContainer?: ViewContainerRef, parentData?: T): void; isExpanded(dataNode: T): boolean; levelAccessor?: (dataNode: T) => number; @@ -103,18 +107,21 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, _nodeDefs: QueryList>; // (undocumented) _nodeOutlet: CdkTreeNodeOutlet; - renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; + nodeType?: 'flat' | 'nested'; + _registerNode(node: CdkTreeNode): void; + _renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; toggle(dataNode: T): void; toggleDescendants(dataNode: T): void; trackBy: TrackByFunction; // @deprecated treeControl?: TreeControl; + _unregisterNode(node: CdkTreeNode): void; readonly viewChange: BehaviorSubject<{ start: number; end: number; }>; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-tree", ["cdkTree"], { "dataSource": "dataSource"; "treeControl": "treeControl"; "levelAccessor": "levelAccessor"; "childrenAccessor": "childrenAccessor"; "trackBy": "trackBy"; "expansionKey": "expansionKey"; }, {}, ["_nodeDefs"], never, false, never>; + static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-tree", ["cdkTree"], { "dataSource": { "alias": "dataSource"; "required": false; }; "treeControl": { "alias": "treeControl"; "required": false; }; "levelAccessor": { "alias": "levelAccessor"; "required": false; }; "childrenAccessor": { "alias": "childrenAccessor"; "required": false; }; "trackBy": { "alias": "trackBy"; "required": false; }; "expansionKey": { "alias": "expansionKey"; "required": false; }; "nodeType": { "alias": "nodeType"; "required": false; }; }, {}, ["_nodeDefs"], never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -141,6 +148,8 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit // (undocumented) protected _elementRef: ElementRef; focus(): void; + _getPositionInSet(): number; + _getSetSize(): number; // (undocumented) isExpandable: boolean; // (undocumented) @@ -161,7 +170,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit // (undocumented) protected _tree: CdkTree; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-tree-node", ["cdkTreeNode"], { "role": "role"; "isExpandable": "isExpandable"; "isExpanded": "isExpanded"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-tree-node", ["cdkTreeNode"], { "role": { "alias": "role"; "required": false; }; "isExpandable": { "alias": "isExpandable"; "required": false; }; "isExpanded": { "alias": "isExpanded"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -173,7 +182,7 @@ export class CdkTreeNodeDef { template: TemplateRef; when: (index: number, nodeData: T) => boolean; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeDef]", never, { "when": "cdkTreeNodeDefWhen"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeDef]", never, { "when": { "alias": "cdkTreeNodeDefWhen"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -220,7 +229,7 @@ export class CdkTreeNodePadding implements OnDestroy { // (undocumented) _setPadding(forceChange?: boolean): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodePadding]", never, { "level": "cdkTreeNodePadding"; "indent": "cdkTreeNodePaddingIndent"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodePadding]", never, { "level": { "alias": "cdkTreeNodePadding"; "required": false; }; "indent": { "alias": "cdkTreeNodePaddingIndent"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, [null, null, null, { optional: true; }]>; } @@ -239,12 +248,12 @@ export class CdkTreeNodeToggle { // (undocumented) protected _treeNode: CdkTreeNode; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeToggle]", never, { "recursive": "cdkTreeNodeToggleRecursive"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeToggle]", never, { "recursive": { "alias": "cdkTreeNodeToggleRecursive"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } -// @public +// @public @deprecated export class FlatTreeControl extends BaseTreeControl { constructor(getLevel: (dataNode: T) => number, isExpandable: (dataNode: T) => boolean, options?: FlatTreeControlOptions | undefined); expandAll(): void; @@ -267,10 +276,10 @@ export interface FlatTreeControlOptions { export function getMultipleTreeControlsError(): Error; // @public -export function getTreeControlFunctionsMissingError(): Error; +export function getTreeControlMissingError(): Error; // @public -export function getTreeControlMissingError(): Error; +export function getTreeControlNodeTypeUnspecifiedError(): Error; // @public export function getTreeMissingMatchingNodeDefError(): Error; @@ -281,7 +290,7 @@ export function getTreeMultipleDefaultNodeDefsError(): Error; // @public export function getTreeNoValidDataSourceError(): Error; -// @public +// @public @deprecated export class NestedTreeControl extends BaseTreeControl { constructor(getChildren: (dataNode: T) => Observable | T[] | undefined | null, options?: NestedTreeControlOptions | undefined); expandAll(): void; @@ -299,7 +308,7 @@ export interface NestedTreeControlOptions { trackBy?: (dataNode: T) => K; } -// @public +// @public @deprecated export interface TreeControl { collapse(dataNode: T): void; collapseAll(): void;