diff --git a/src/cdk/tree/nested-node.ts b/src/cdk/tree/nested-node.ts index d4ced02bb185..633ab70eca8a 100644 --- a/src/cdk/tree/nested-node.ts +++ b/src/cdk/tree/nested-node.ts @@ -95,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/tree.ts b/src/cdk/tree/tree.ts index 5734a89cc682..f71615b56a53 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -86,6 +86,18 @@ function coerceObservable(data: T | Observable): Observable { return data; } +type RenderingData = + | { + flattenedNodes: null; + nodeType: null; + renderNodes: T[]; + } + | { + flattenedNodes: T[]; + nodeType: 'nested' | 'flat'; + renderNodes: []; + }; + /** * CDK tree component that connects with a data source to retrieve data of type `T` and renders * dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source. @@ -256,29 +268,13 @@ export class CdkTree private _elementRef: ElementRef, ) {} - ngOnInit() { - this._dataDiffer = this._differs.find([]).create(this.trackBy); - if (typeof ngDevMode === 'undefined' || ngDevMode) { - // Verify that Tree follows API contract of using one of TreeControl, levelAccessor or - // childrenAccessor. Throw an appropriate error if contract is not met. - let numTreeControls = 0; - - if (this.treeControl) { - numTreeControls++; - } - if (this.levelAccessor) { - numTreeControls++; - } - if (this.childrenAccessor) { - numTreeControls++; - } + ngAfterContentInit() { + this._initializeKeyManager(); + } - if (!numTreeControls) { - throw getTreeControlMissingError(); - } else if (numTreeControls > 1) { - throw getMultipleTreeControlsError(); - } - } + ngAfterContentChecked() { + this._updateDefaultNodeDefinition(); + this._subscribeToDataChanges(); } ngOnDestroy() { @@ -298,51 +294,17 @@ export class CdkTree } } - ngAfterContentInit() { - const items = combineLatest([this._keyManagerNodes, this._nodes]).pipe( - map(([keyManagerNodes, renderNodes]) => - keyManagerNodes.reduce[]>((items, data) => { - const node = renderNodes.get(this._getExpansionKey(data)); - if (node) { - items.push(node); - } - return items; - }, []), - ), - ); - - const keyManagerOptions: TreeKeyManagerOptions> = { - trackBy: node => this._getExpansionKey(node.data), - skipPredicate: node => !!node.isDisabled, - typeAheadDebounceInterval: true, - horizontalOrientation: this._dir.value, - }; - - this._keyManager = this._keyManagerFactory(items, keyManagerOptions); - - this._keyManager.change - .pipe(startWith(null), pairwise(), takeUntil(this._onDestroy)) - .subscribe(([prev, next]) => { - prev?._setTabUnfocusable(); - next?._setTabFocusable(); - }); - - this._keyManager.change.pipe(startWith(null), takeUntil(this._onDestroy)).subscribe(() => { - // refresh the tabindex when the active item changes. - this._setTabIndex(); - }); + ngOnInit() { + this._checkTreeControlUsage(); + this._initializeDataDiffer(); } - ngAfterContentChecked() { + private _updateDefaultNodeDefinition() { const defaultNodeDefs = this._nodeDefs.filter(def => !def.when); if (defaultNodeDefs.length > 1 && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw getTreeMultipleDefaultNodeDefsError(); } this._defaultNodeDef = defaultNodeDefs[0]; - - if (this.dataSource && this._nodeDefs && !this._dataSubscription) { - this._observeRenderChanges(); - } } /** @@ -396,12 +358,16 @@ export class CdkTree this._dataSource = dataSource; if (this._nodeDefs) { - this._observeRenderChanges(); + this._subscribeToDataChanges(); } } /** Set up a subscription for the data provided by the data source. */ - private _observeRenderChanges() { + private _subscribeToDataChanges() { + if (this._dataSubscription) { + return; + } + let dataStream: Observable | undefined; if (isDataSource(this._dataSource)) { @@ -412,6 +378,13 @@ export class CdkTree dataStream = observableOf(this._dataSource); } + if (!dataStream) { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + throw getTreeNoValidDataSourceError(); + } + return; + } + let expansionModel; if (!this.treeControl) { this._expansionModel = new SelectionModel(true); @@ -420,49 +393,48 @@ export class CdkTree expansionModel = this.treeControl.expansionModel; } - if (dataStream) { - this._dataSubscription = combineLatest([ - dataStream, - this._nodeType, - // NB: the data is unused below, however we add it here to essentially - // trigger data rendering when expansion changes occur. - expansionModel.changed.pipe( - startWith(null), - tap(expansionChanges => { - this._emitExpansionChanges(expansionChanges); - }), - ), - ]) - .pipe( - switchMap(([data, nodeType]) => { - if (nodeType === null) { - return observableOf([{renderNodes: data}, nodeType] as const); - } - - // If we're here, then we know what our node type is, and therefore can - // perform our usual rendering pipeline, which necessitates converting the data - return this._convertData(data, nodeType).pipe( - map(convertedData => [convertedData, nodeType] as const), - ); - }), - takeUntil(this._onDestroy), - ) - .subscribe(([data, nodeType]) => { + this._dataSubscription = combineLatest([ + dataStream, + this._nodeType, + // NB: the data is unused below, however we add it here to essentially + // trigger data rendering when expansion changes occur. + expansionModel.changed.pipe( + startWith(null), + tap(expansionChanges => { + this._emitExpansionChanges(expansionChanges); + }), + ), + ]) + .pipe( + switchMap(([data, nodeType]) => { if (nodeType === null) { - // Skip saving cached and key manager data. - this.renderNodeChanges(data.renderNodes); - return; + return observableOf([{renderNodes: data}, nodeType] as const); } // If we're here, then we know what our node type is, and therefore can - // perform our usual rendering pipeline. - this._updateCachedData(data.flattenedNodes); - this.renderNodeChanges(data.renderNodes); - this._updateKeyManagerItems(data.flattenedNodes); - }); - } else if (typeof ngDevMode === 'undefined' || ngDevMode) { - throw getTreeNoValidDataSourceError(); + // perform our usual rendering pipeline, which necessitates converting the data + return this._convertData(data, nodeType).pipe( + map(convertedData => [convertedData, nodeType] as const), + ); + }), + takeUntil(this._onDestroy), + ) + .subscribe(([data, nodeType]) => { + this._renderDataChanges({nodeType, ...data} as RenderingData); + }); + } + + private _renderDataChanges(data: RenderingData) { + if (data.nodeType === null) { + this._renderNodeChanges(data.renderNodes); + return; } + + // If we're here, then we know what our node type is, and therefore can + // perform our usual rendering pipeline. + this._updateCachedData(data.flattenedNodes); + this._renderNodeChanges(data.renderNodes); + this._updateKeyManagerItems(data.flattenedNodes); } private _emitExpansionChanges(expansionChanges: SelectionChange | null) { @@ -481,8 +453,71 @@ export class CdkTree } } + private _initializeKeyManager() { + const items = combineLatest([this._keyManagerNodes, this._nodes]).pipe( + map(([keyManagerNodes, renderNodes]) => + keyManagerNodes.reduce[]>((items, data) => { + const node = renderNodes.get(this._getExpansionKey(data)); + if (node) { + items.push(node); + } + return items; + }, []), + ), + ); + + const keyManagerOptions: TreeKeyManagerOptions> = { + trackBy: node => this._getExpansionKey(node.data), + skipPredicate: node => !!node.isDisabled, + typeAheadDebounceInterval: true, + horizontalOrientation: this._dir.value, + }; + + this._keyManager = this._keyManagerFactory(items, keyManagerOptions); + + this._keyManager.change + .pipe(startWith(null), pairwise(), takeUntil(this._onDestroy)) + .subscribe(([prev, next]) => { + prev?._setTabUnfocusable(); + next?._setTabFocusable(); + }); + + this._keyManager.change.pipe(startWith(null), takeUntil(this._onDestroy)).subscribe(() => { + // refresh the tabindex when the active item changes. + this._setTabIndex(); + }); + } + + private _initializeDataDiffer() { + this._dataDiffer = this._differs.find([]).create(this.trackBy); + } + + private _checkTreeControlUsage() { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + // Verify that Tree follows API contract of using one of TreeControl, levelAccessor or + // childrenAccessor. Throw an appropriate error if contract is not met. + let numTreeControls = 0; + + if (this.treeControl) { + numTreeControls++; + } + if (this.levelAccessor) { + numTreeControls++; + } + if (this.childrenAccessor) { + numTreeControls++; + } + + if (!numTreeControls) { + throw getTreeControlMissingError(); + } else if (numTreeControls > 1) { + throw getMultipleTreeControlsError(); + } + } + } + /** 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,