From 65ec485ce518b3fb3269235d71271e60c7d7832e Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Tue, 14 Nov 2023 21:56:33 +0000 Subject: [PATCH] refactor(cdk/tree): simplify code in lifecycle hooks Refactor CdkTree implementation to simplify code in lifecycle hooks. Create a RenderingData type definition to simplify the Tree's rendering pipeline. The render pipeline looks like this. It has two entry points ngAfterContentChecked and whener the datasource is switched. 1. ngAfterContentChecked OR datasource changed 2. call _subscribeToDataChanges From here, the pipeline diverges. Tree subscribes to changes in the datasource and expansion state. From there, the pipeline goes on two paths: emit changes to expansion state, and render the changes from the datasource. Add RenderingData type to make it more clear what stages are in the pipeline. This commit message will be squashed away. --- src/cdk/tree/nested-node.ts | 2 +- src/cdk/tree/tree.ts | 237 +++++++++++++++++++++--------------- 2 files changed, 137 insertions(+), 102 deletions(-) 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,