From f9b9bdd800e58806491e60e0d880e0d9931313ab Mon Sep 17 00:00:00 2001 From: friendlyAce Date: Sat, 31 Aug 2024 19:28:14 +0200 Subject: [PATCH 1/2] feat(template): add DataSource support for rx-virtual-for directive --- .../virtual-scrolling/src/index.ts | 2 + .../virtual-scrolling/src/lib/model.ts | 11 ++++++ .../src/lib/virtual-for.directive.ts | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/libs/template/experimental/virtual-scrolling/src/index.ts b/libs/template/experimental/virtual-scrolling/src/index.ts index 8c50f1ed31..86b340861c 100644 --- a/libs/template/experimental/virtual-scrolling/src/index.ts +++ b/libs/template/experimental/virtual-scrolling/src/index.ts @@ -1,4 +1,6 @@ export { + CollectionViewer, + DataSource, ListRange, RxVirtualForViewContext, RxVirtualScrollElement, diff --git a/libs/template/experimental/virtual-scrolling/src/lib/model.ts b/libs/template/experimental/virtual-scrolling/src/lib/model.ts index aacb1d3e84..c6ed5c35f9 100644 --- a/libs/template/experimental/virtual-scrolling/src/lib/model.ts +++ b/libs/template/experimental/virtual-scrolling/src/lib/model.ts @@ -34,6 +34,17 @@ export interface ListRange { end: number; } +export abstract class DataSource { + abstract connect( + collectionViewer: CollectionViewer, + ): Observable>; + abstract disconnect(collectionViewer: CollectionViewer): void; +} + +export interface CollectionViewer { + viewChange: Observable; +} + /** * @Directive RxVirtualScrollStrategy * diff --git a/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts b/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts index 4b39c5d0cf..2da85fd4b7 100644 --- a/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts +++ b/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts @@ -34,6 +34,7 @@ import { Promise } from '@rx-angular/cdk/zone-less/browser'; import { combineLatest, concat, + ConnectableObservable, isObservable, MonoTypeOperatorFunction, NEVER, @@ -55,6 +56,8 @@ import { tap, } from 'rxjs/operators'; import { + CollectionViewer, + DataSource, ListRange, RxVirtualForViewContext, RxVirtualScrollStrategy, @@ -241,6 +244,7 @@ export class RxVirtualFor = NgIterable> potentialSignalOrObservable: | Observable<(U & NgIterable) | undefined | null> | Signal<(U & NgIterable) | undefined | null> + | DataSource | (U & NgIterable) | null | undefined, @@ -251,6 +255,21 @@ export class RxVirtualFor = NgIterable> this.observables$.next( toObservable(potentialSignalOrObservable, { injector: this.injector }), ); + } else if (this.isDataSource(potentialSignalOrObservable)) { + this.staticValue = undefined; + this.renderStatic = false; + + const collectionViewer: CollectionViewer = { + viewChange: this.scrollStrategy.renderedRange$, + }; + + this.observables$.next( + potentialSignalOrObservable.connect(collectionViewer), + ); + + this._destroy$.pipe(take(1)).subscribe(() => { + potentialSignalOrObservable.disconnect(collectionViewer); + }); } else if (!isObservable(potentialSignalOrObservable)) { this.staticValue = potentialSignalOrObservable; this.renderStatic = true; @@ -261,6 +280,24 @@ export class RxVirtualFor = NgIterable> } } + /** @internal */ + private isDataSource( + value: + | (U & NgIterable) + | Observable> + | DataSource + | null + | undefined, + ): value is DataSource { + return ( + value !== null && + value !== undefined && + 'connect' in value && + typeof value.connect === 'function' && + !(value instanceof ConnectableObservable) + ); + } + /** * @internal * A reference to the template that is created for each item in the iterable. From 09270f99f65dc3579a9030d2a2e0b93a09e16ebf Mon Sep 17 00:00:00 2001 From: friendlyAce Date: Sat, 24 May 2025 22:28:28 +0200 Subject: [PATCH 2/2] fix(template): improve DataSource lifecycle management in RxVirtualFor - Renamed potentialSignalOrObservable to potentialSignalOrObservableOrDataSource - Move DataSource disconnect from subscription to ngOnDestroy lifecycle - Add disconnectDataSource() method for centralized cleanup logic - Ensure disconnect and cleanup of previous data source on data source input changes to prevent memory leaks --- .../src/lib/virtual-for.directive.ts | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts b/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts index 2da85fd4b7..174faf3eef 100644 --- a/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts +++ b/libs/template/experimental/virtual-scrolling/src/lib/virtual-for.directive.ts @@ -216,6 +216,11 @@ export class RxVirtualFor = NgIterable> optional: true, }); + /** @internal */ + private connectedDataSource?: DataSource; + /** @internal */ + private collectionViewer?: CollectionViewer; + /** @internal */ private _differ?: IterableDiffer; @@ -237,11 +242,11 @@ export class RxVirtualFor = NgIterable> * [hero]="hero"> * * - * @param potentialSignalOrObservable + * @param potentialSignalOrObservableOrDataSource */ @Input() set rxVirtualForOf( - potentialSignalOrObservable: + potentialSignalOrObservableOrDataSource: | Observable<(U & NgIterable) | undefined | null> | Signal<(U & NgIterable) | undefined | null> | DataSource @@ -249,13 +254,17 @@ export class RxVirtualFor = NgIterable> | null | undefined, ) { - if (isSignal(potentialSignalOrObservable)) { + if (isSignal(potentialSignalOrObservableOrDataSource)) { this.staticValue = undefined; this.renderStatic = false; this.observables$.next( - toObservable(potentialSignalOrObservable, { injector: this.injector }), + toObservable(potentialSignalOrObservableOrDataSource, { + injector: this.injector, + }), ); - } else if (this.isDataSource(potentialSignalOrObservable)) { + } else if (this.isDataSource(potentialSignalOrObservableOrDataSource)) { + this.disconnectDataSource(); + this.staticValue = undefined; this.renderStatic = false; @@ -263,20 +272,19 @@ export class RxVirtualFor = NgIterable> viewChange: this.scrollStrategy.renderedRange$, }; + this.collectionViewer = collectionViewer; + this.connectedDataSource = potentialSignalOrObservableOrDataSource; + this.observables$.next( - potentialSignalOrObservable.connect(collectionViewer), + potentialSignalOrObservableOrDataSource.connect(collectionViewer), ); - - this._destroy$.pipe(take(1)).subscribe(() => { - potentialSignalOrObservable.disconnect(collectionViewer); - }); - } else if (!isObservable(potentialSignalOrObservable)) { - this.staticValue = potentialSignalOrObservable; + } else if (!isObservable(potentialSignalOrObservableOrDataSource)) { + this.staticValue = potentialSignalOrObservableOrDataSource; this.renderStatic = true; } else { this.staticValue = undefined; this.renderStatic = false; - this.observables$.next(potentialSignalOrObservable); + this.observables$.next(potentialSignalOrObservableOrDataSource); } } @@ -290,8 +298,7 @@ export class RxVirtualFor = NgIterable> | undefined, ): value is DataSource { return ( - value !== null && - value !== undefined && + value != null && 'connect' in value && typeof value.connect === 'function' && !(value instanceof ConnectableObservable) @@ -677,8 +684,18 @@ export class RxVirtualFor = NgIterable> } } + /** @internal */ + private disconnectDataSource(): void { + if (this.connectedDataSource && this.collectionViewer) { + this.connectedDataSource.disconnect(this.collectionViewer); + this.connectedDataSource = undefined; + this.collectionViewer = undefined; + } + } + /** @internal */ ngOnDestroy() { + this.disconnectDataSource(); this._destroy$.next(); this.templateManager.detach(); }