diff --git a/components/affix/affix.component.ts b/components/affix/affix.component.ts index f8813f16477..567e6c7c763 100644 --- a/components/affix/affix.component.ts +++ b/components/affix/affix.component.ts @@ -11,11 +11,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, ElementRef, EventEmitter, inject, Input, - NgZone, OnChanges, OnDestroy, OnInit, @@ -25,14 +25,20 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { fromEvent, merge, ReplaySubject, Subject, Subscription } from 'rxjs'; -import { map, takeUntil, throttleTime } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { merge, ReplaySubject, Subscription } from 'rxjs'; +import { map, throttleTime } from 'rxjs/operators'; import { NzResizeObserver } from 'ng-zorro-antd/cdk/resize-observer'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; import { NzScrollService } from 'ng-zorro-antd/core/services'; import { NgStyleInterface } from 'ng-zorro-antd/core/types'; -import { getStyleAsText, numberAttributeWithZeroFallback, shallowEqual } from 'ng-zorro-antd/core/util'; +import { + fromEventOutsideAngular, + getStyleAsText, + numberAttributeWithZeroFallback, + shallowEqual +} from 'ng-zorro-antd/core/util'; import { AffixRespondEvents } from './respond-events'; import { getTargetRect, SimpleRect } from './utils'; @@ -78,7 +84,6 @@ export class NzAffixComponent implements AfterViewInit, OnChanges, OnDestroy, On private placeholderStyle?: NgStyleInterface; private positionChangeSubscription: Subscription = Subscription.EMPTY; private offsetChanged$ = new ReplaySubject(1); - private destroy$ = new Subject(); private timeout?: ReturnType; private document: Document = inject(DOCUMENT); @@ -87,11 +92,12 @@ export class NzAffixComponent implements AfterViewInit, OnChanges, OnDestroy, On return (typeof el === 'string' ? this.document.querySelector(el) : el) || window; } + private destroyRef = inject(DestroyRef); + constructor( el: ElementRef, public nzConfigService: NzConfigService, private scrollSrv: NzScrollService, - private ngZone: NgZone, private platform: Platform, private renderer: Renderer2, private nzResizeObserver: NzResizeObserver, @@ -103,7 +109,7 @@ export class NzAffixComponent implements AfterViewInit, OnChanges, OnDestroy, On } ngOnInit(): void { - this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.directionality.change?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((direction: Direction) => { this.dir = direction; this.registerListeners(); this.updatePosition({} as Event); @@ -139,23 +145,23 @@ export class NzAffixComponent implements AfterViewInit, OnChanges, OnDestroy, On this.removeListeners(); const el = this.target === window ? this.document.body : (this.target as Element); - this.positionChangeSubscription = this.ngZone.runOutsideAngular(() => - merge( - ...Object.keys(AffixRespondEvents).map(evName => fromEvent(this.target, evName)), - this.offsetChanged$.pipe(map(() => ({}))), - this.nzResizeObserver.observe(el) + this.positionChangeSubscription = merge( + ...Object.keys(AffixRespondEvents).map(evName => fromEventOutsideAngular(this.target, evName)), + this.offsetChanged$.pipe(map(() => ({}))), + this.nzResizeObserver.observe(el) + ) + .pipe( + throttleTime(NZ_AFFIX_DEFAULT_SCROLL_TIME, undefined, { trailing: true }), + takeUntilDestroyed(this.destroyRef) ) - .pipe(throttleTime(NZ_AFFIX_DEFAULT_SCROLL_TIME, undefined, { trailing: true }), takeUntil(this.destroy$)) - .subscribe(e => this.updatePosition(e as Event)) - ); + .subscribe(e => this.updatePosition(e as Event)); + this.timeout = setTimeout(() => this.updatePosition({} as Event)); } private removeListeners(): void { clearTimeout(this.timeout); this.positionChangeSubscription.unsubscribe(); - this.destroy$.next(true); - this.destroy$.complete(); } getOffset(element: Element, target: Element | Window | undefined): SimpleRect { diff --git a/components/image/image.directive.ts b/components/image/image.directive.ts index e0dac6debae..c54a6cbfed6 100644 --- a/components/image/image.directive.ts +++ b/components/image/image.directive.ts @@ -7,16 +7,17 @@ import { Direction, Directionality } from '@angular/cdk/bidi'; import { DOCUMENT } from '@angular/common'; import { ChangeDetectorRef, + DestroyRef, Directive, ElementRef, Input, OnChanges, - OnDestroy, OnInit, SimpleChanges, booleanAttribute, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Subject, fromEvent } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -39,7 +40,7 @@ export type NzImageScaleStep = number; '(click)': 'onPreview()' } }) -export class NzImageDirective implements OnInit, OnChanges, OnDestroy { +export class NzImageDirective implements OnInit, OnChanges { readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME; @Input() nzSrc = ''; @@ -53,9 +54,9 @@ export class NzImageDirective implements OnInit, OnChanges, OnDestroy { backLoadImage!: HTMLImageElement; status: ImageStatusType = 'normal'; private backLoadDestroy$ = new Subject(); - private destroy$ = new Subject(); private document: Document = inject(DOCUMENT); private parentGroup = inject(NzImageGroupComponent, { optional: true }); + private destroyRef = inject(DestroyRef); get previewable(): boolean { return !this.nzDisablePreview && this.status !== 'error'; @@ -75,7 +76,7 @@ export class NzImageDirective implements OnInit, OnChanges, OnDestroy { this.parentGroup.addImage(this); } if (this.directionality) { - this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.directionality.change?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((direction: Direction) => { this.dir = direction; this.cdr.detectChanges(); }); @@ -83,11 +84,6 @@ export class NzImageDirective implements OnInit, OnChanges, OnDestroy { } } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - onPreview(): void { if (!this.previewable) { return; @@ -146,40 +142,40 @@ export class NzImageDirective implements OnInit, OnChanges, OnDestroy { this.backLoadImage.srcset = this.nzSrcset; this.status = 'loading'; + const element = this.elementRef.nativeElement; + // unsubscribe last backLoad this.backLoadDestroy$.next(); this.backLoadDestroy$.complete(); this.backLoadDestroy$ = new Subject(); if (this.backLoadImage.complete) { this.status = 'normal'; - this.getElement().nativeElement.src = this.nzSrc; - this.getElement().nativeElement.srcset = this.nzSrcset; + element.src = this.nzSrc; + element.srcset = this.nzSrcset; } else { if (this.nzPlaceholder) { - this.getElement().nativeElement.src = this.nzPlaceholder; - this.getElement().nativeElement.srcset = ''; + element.src = this.nzPlaceholder; + element.srcset = ''; } else { - this.getElement().nativeElement.src = this.nzSrc; - this.getElement().nativeElement.srcset = this.nzSrcset; + element.src = this.nzSrc; + element.srcset = this.nzSrcset; } - // The `nz-image` directive can be destroyed before the `load` or `error` event is dispatched, - // so there's no sense to keep capturing `this`. fromEvent(this.backLoadImage, 'load') - .pipe(takeUntil(this.backLoadDestroy$), takeUntil(this.destroy$)) + .pipe(takeUntil(this.backLoadDestroy$), takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.status = 'normal'; - this.getElement().nativeElement.src = this.nzSrc; - this.getElement().nativeElement.srcset = this.nzSrcset; + element.src = this.nzSrc; + element.srcset = this.nzSrcset; }); fromEvent(this.backLoadImage, 'error') - .pipe(takeUntil(this.backLoadDestroy$), takeUntil(this.destroy$)) + .pipe(takeUntil(this.backLoadDestroy$), takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.status = 'error'; if (this.nzFallback) { - this.getElement().nativeElement.src = this.nzFallback; - this.getElement().nativeElement.srcset = ''; + element.src = this.nzFallback; + element.srcset = ''; } }); } diff --git a/components/table/src/table/table-inner-scroll.component.ts b/components/table/src/table/table-inner-scroll.component.ts index 9519e2f2e69..60d3a4f7f51 100644 --- a/components/table/src/table/table-inner-scroll.component.ts +++ b/components/table/src/table/table-inner-scroll.component.ts @@ -10,7 +10,9 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + DestroyRef, ElementRef, + inject, Input, NgZone, OnChanges, @@ -22,8 +24,9 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { merge, Subject } from 'rxjs'; -import { delay, filter, startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { delay, filter, startWith, switchMap } from 'rxjs/operators'; import { NzResizeService } from 'ng-zorro-antd/core/services'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; @@ -134,7 +137,6 @@ export class NzTableInnerScrollComponent implements OnChanges, AfterViewInit, @Input() noDataVirtualHeight = '182px'; private data$ = new Subject(); private scroll$ = new Subject(); - private destroy$ = new Subject(); setScrollPositionClassName(clear: boolean = false): void { const { scrollWidth, scrollLeft, clientWidth } = this.tableBodyElement.nativeElement; @@ -155,6 +157,8 @@ export class NzTableInnerScrollComponent implements OnChanges, AfterViewInit, } } + private destroyRef = inject(DestroyRef); + constructor( private renderer: Renderer2, private ngZone: NgZone, @@ -187,36 +191,32 @@ export class NzTableInnerScrollComponent implements OnChanges, AfterViewInit, ngAfterViewInit(): void { if (this.platform.isBrowser) { - this.ngZone.runOutsideAngular(() => { - const scrollEvent$ = this.scroll$.pipe( - startWith(null), - delay(0), - switchMap(() => - fromEventOutsideAngular(this.tableBodyElement.nativeElement, 'scroll').pipe(startWith(true)) - ), - takeUntil(this.destroy$) - ); - const resize$ = this.resizeService.subscribe().pipe(takeUntil(this.destroy$)); - const data$ = this.data$.pipe(takeUntil(this.destroy$)); - const setClassName$ = merge(scrollEvent$, resize$, data$, this.scroll$).pipe( - startWith(true), - delay(0), - takeUntil(this.destroy$) - ); - setClassName$.subscribe(() => this.setScrollPositionClassName()); - scrollEvent$.pipe(filter(() => !!this.scrollY)).subscribe(() => { - this.tableHeaderElement.nativeElement.scrollLeft = this.tableBodyElement.nativeElement.scrollLeft; - if (this.tableFootElement) { - this.tableFootElement.nativeElement.scrollLeft = this.tableBodyElement.nativeElement.scrollLeft; - } - }); + const scrollEvent$ = this.scroll$.pipe( + startWith(null), + delay(0), + switchMap(() => + fromEventOutsideAngular(this.tableBodyElement.nativeElement, 'scroll').pipe(startWith(true)) + ), + takeUntilDestroyed(this.destroyRef) + ); + const resize$ = this.resizeService.subscribe().pipe(takeUntilDestroyed(this.destroyRef)); + const data$ = this.data$.pipe(takeUntilDestroyed(this.destroyRef)); + const setClassName$ = merge(scrollEvent$, resize$, data$, this.scroll$).pipe( + startWith(true), + delay(0), + takeUntilDestroyed(this.destroyRef) + ); + setClassName$.subscribe(() => this.setScrollPositionClassName()); + scrollEvent$.pipe(filter(() => !!this.scrollY)).subscribe(() => { + this.tableHeaderElement.nativeElement.scrollLeft = this.tableBodyElement.nativeElement.scrollLeft; + if (this.tableFootElement) { + this.tableFootElement.nativeElement.scrollLeft = this.tableBodyElement.nativeElement.scrollLeft; + } }); } } ngOnDestroy(): void { this.setScrollPositionClassName(true); - this.destroy$.next(); - this.destroy$.complete(); } } diff --git a/components/tabs/tab-scroll-list.directive.ts b/components/tabs/tab-scroll-list.directive.ts index 216b13d9540..29e6a6c4055 100644 --- a/components/tabs/tab-scroll-list.directive.ts +++ b/components/tabs/tab-scroll-list.directive.ts @@ -3,8 +3,11 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; -import { fromEvent, Observable, Subscription } from 'rxjs'; +import { DestroyRef, Directive, ElementRef, EventEmitter, inject, NgZone, OnInit, Output } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable } from 'rxjs'; + +import { fromEventOutsideAngular } from 'ng-zorro-antd/core/util'; import { NzTabScrollEvent, @@ -21,7 +24,7 @@ const SPEED_OFF_MULTIPLE = 0.995 ** REFRESH_INTERVAL; @Directive({ selector: '[nzTabScrollList]' }) -export class NzTabScrollListDirective implements OnInit, OnDestroy { +export class NzTabScrollListDirective implements OnInit { lastWheelDirection: 'x' | 'y' | null = null; lastWheelTimestamp = 0; lastTimestamp = 0; @@ -32,43 +35,36 @@ export class NzTabScrollListDirective implements OnInit, OnDestroy { lastOffset: NzTabScrollListOffset | null = null; motion = -1; - unsubscribe: () => void = () => void 0; - @Output() readonly offsetChange = new EventEmitter(); @Output() readonly tabScroll = new EventEmitter(); + private destroyRef = inject(DestroyRef); + constructor( private ngZone: NgZone, private elementRef: ElementRef ) {} ngOnInit(): void { - this.unsubscribe = this.ngZone.runOutsideAngular(() => { - const el = this.elementRef.nativeElement; - - const wheel$ = fromEvent(el, 'wheel'); - const touchstart$ = fromEvent(el, 'touchstart'); - const touchmove$ = fromEvent(el, 'touchmove'); - const touchend$ = fromEvent(el, 'touchend'); - - const subscription = new Subscription(); - subscription.add(this.subscribeWrap('wheel', wheel$, this.onWheel)); - subscription.add(this.subscribeWrap('touchstart', touchstart$, this.onTouchStart)); - subscription.add(this.subscribeWrap('touchmove', touchmove$, this.onTouchMove)); - subscription.add(this.subscribeWrap('touchend', touchend$, this.onTouchEnd)); - - return () => { - subscription.unsubscribe(); - }; - }); + const el = this.elementRef.nativeElement; + + const wheel$ = fromEventOutsideAngular(el, 'wheel'); + const touchstart$ = fromEventOutsideAngular(el, 'touchstart'); + const touchmove$ = fromEventOutsideAngular(el, 'touchmove'); + const touchend$ = fromEventOutsideAngular(el, 'touchend'); + + this.subscribeWrap('wheel', wheel$, this.onWheel); + this.subscribeWrap('touchstart', touchstart$, this.onTouchStart); + this.subscribeWrap('touchmove', touchmove$, this.onTouchMove); + this.subscribeWrap('touchend', touchend$, this.onTouchEnd); } subscribeWrap( type: NzTabScrollEvent['type'], observable: Observable, handler: NzTabScrollEventHandlerFun - ): Subscription { - return observable.subscribe(event => { + ): void { + observable.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => { this.tabScroll.emit({ type, event @@ -173,16 +169,14 @@ export class NzTabScrollListDirective implements OnInit, OnDestroy { }; onOffset(x: number, y: number, event: Event): void { - this.ngZone.run(() => { - this.offsetChange.emit({ - x, - y, - event + if (this.offsetChange.observed) { + this.ngZone.run(() => { + this.offsetChange.emit({ + x, + y, + event + }); }); - }); - } - - ngOnDestroy(): void { - this.unsubscribe(); + } } } diff --git a/components/upload/upload-list.component.ts b/components/upload/upload-list.component.ts index 94df444baf1..31bcec2a6e0 100644 --- a/components/upload/upload-list.component.ts +++ b/components/upload/upload-list.component.ts @@ -11,18 +11,20 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, Input, NgZone, OnChanges, - OnDestroy, ViewEncapsulation, inject } from '@angular/core'; -import { Observable, Subject, fromEvent, of } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; +import { fromEventOutsideAngular } from 'ng-zorro-antd/core/util'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzProgressModule } from 'ng-zorro-antd/progress'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; @@ -67,7 +69,7 @@ interface UploadListFile extends NzUploadFile { changeDetection: ChangeDetectionStrategy.OnPush, imports: [NzToolTipModule, NgTemplateOutlet, NzIconModule, NzButtonModule, NzProgressModule] }) -export class NzUploadListComponent implements OnChanges, OnDestroy { +export class NzUploadListComponent implements OnChanges { list: UploadListFile[] = []; private get showPic(): boolean { @@ -90,7 +92,7 @@ export class NzUploadListComponent implements OnChanges, OnDestroy { @Input() dir: Direction = 'ltr'; private document: Document = inject(DOCUMENT); - private destroy$ = new Subject(); + private destroyRef = inject(DestroyRef); private genErr(file: NzUploadFile): string { if (file.response && typeof file.response === 'string') { @@ -152,7 +154,7 @@ export class NzUploadListComponent implements OnChanges, OnDestroy { const img = new Image(); const objectUrl = URL.createObjectURL(file); img.src = objectUrl; - return fromEvent(img, 'load').pipe( + return fromEventOutsideAngular(img, 'load').pipe( map(() => { const { width, height } = img; @@ -206,7 +208,7 @@ export class NzUploadListComponent implements OnChanges, OnDestroy { // A promise microtask can be resolved after the view is destroyed. Thus running `detectChanges()` // will cause a runtime exception (`detectChanges()` cannot be run on destroyed views). const dataUrl$ = (this.previewFile ? this.previewFile(file) : this.previewImage(file.originFileObj!)).pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ); this.ngZone.runOutsideAngular(() => { dataUrl$.subscribe(dataUrl => { @@ -276,8 +278,4 @@ export class NzUploadListComponent implements OnChanges, OnDestroy { this.fixData(); this.genThumb(); } - - ngOnDestroy(): void { - this.destroy$.next(); - } }