diff --git a/src/cdk/scrolling/virtual-scroll-viewport.ts b/src/cdk/scrolling/virtual-scroll-viewport.ts index 6eabf6c10190..7266e24db197 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.ts @@ -8,7 +8,9 @@ import {Directionality} from '@angular/cdk/bidi'; import {ListRange} from '@angular/cdk/collections'; +import {Platform} from '@angular/cdk/platform'; import { + afterNextRender, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, @@ -16,6 +18,7 @@ import { ElementRef, inject, Inject, + Injector, Input, NgZone, OnDestroy, @@ -25,21 +28,20 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; -import {Platform} from '@angular/cdk/platform'; import { animationFrameScheduler, asapScheduler, Observable, - Subject, Observer, + Subject, Subscription, } from 'rxjs'; import {auditTime, startWith, takeUntil} from 'rxjs/operators'; import {ScrollDispatcher} from './scroll-dispatcher'; import {CdkScrollable, ExtendedScrollToOptions} from './scrollable'; -import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy'; import {ViewportRuler} from './viewport-ruler'; import {CdkVirtualScrollRepeater} from './virtual-scroll-repeater'; +import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy'; import {CdkVirtualScrollable, VIRTUAL_SCROLLABLE} from './virtual-scrollable'; /** Checks if the given ranges are equal. */ @@ -173,6 +175,10 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On /** Subscription to changes in the viewport size. */ private _viewportChanges = Subscription.EMPTY; + private _injector = inject(Injector); + + private _isDestroyed = false; + constructor( public override elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, @@ -250,6 +256,8 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On this._detachedSubject.complete(); this._viewportChanges.unsubscribe(); + this._isDestroyed = true; + super.ngOnDestroy(); } @@ -498,23 +506,34 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On /** Run change detection. */ private _doChangeDetection() { - this._isChangeDetectionPending = false; - - // Apply the content transform. The transform can't be set via an Angular binding because - // bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of - // string literals, a variable that can only be 'X' or 'Y', and user input that is run through - // the `Number` function first to coerce it to a numeric value. - this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform; - // Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection - // from the root, since the repeated items are content projected in. Calling `detectChanges` - // instead does not properly check the projected content. - this.ngZone.run(() => this._changeDetectorRef.markForCheck()); - - const runAfterChangeDetection = this._runAfterChangeDetection; - this._runAfterChangeDetection = []; - for (const fn of runAfterChangeDetection) { - fn(); + if (this._isDestroyed) { + return; } + + this.ngZone.run(() => { + // Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection + // from the root, since the repeated items are content projected in. Calling `detectChanges` + // instead does not properly check the projected content. + this._changeDetectorRef.markForCheck(); + + // Apply the content transform. The transform can't be set via an Angular binding because + // bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of + // string literals, a variable that can only be 'X' or 'Y', and user input that is run through + // the `Number` function first to coerce it to a numeric value. + this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform; + + afterNextRender( + () => { + this._isChangeDetectionPending = false; + const runAfterChangeDetection = this._runAfterChangeDetection; + this._runAfterChangeDetection = []; + for (const fn of runAfterChangeDetection) { + fn(); + } + }, + {injector: this._injector}, + ); + }); } /** Calculates the `style.width` and `style.height` for the spacer element. */ diff --git a/src/dev-app/main.ts b/src/dev-app/main.ts index 8f0ab1aa010e..0c5ceb7cf76f 100644 --- a/src/dev-app/main.ts +++ b/src/dev-app/main.ts @@ -53,6 +53,6 @@ bootstrapApplication(DevApp, { {provide: Directionality, useClass: DevAppDirectionality}, cachedAppState.zoneless ? provideExperimentalZonelessChangeDetection() - : provideZoneChangeDetection({eventCoalescing: true}), + : provideZoneChangeDetection({eventCoalescing: true, runCoalescing: true}), ], });