From 19b401218812bdca10bf448e205e935fa69a4d75 Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Tue, 7 May 2024 21:49:04 +0800 Subject: [PATCH] feat(cdk): add `tuiTakeUntilDestroyed` helper (#7381) Signed-off-by: waterplea --- .../native-validator.directive.ts | 32 ++++++++----------- projects/cdk/observables/index.ts | 1 + .../cdk/observables/take-until-destroyed.ts | 22 +++++++++++++ projects/core/services/position.service.ts | 4 +-- .../appearance/appearance.template.html | 10 ++++-- .../services/table-bar/table-bar.component.ts | 11 ++----- 6 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 projects/cdk/observables/take-until-destroyed.ts diff --git a/projects/cdk/directives/native-validator/native-validator.directive.ts b/projects/cdk/directives/native-validator/native-validator.directive.ts index 846e0734a21c..c738c54604a4 100644 --- a/projects/cdk/directives/native-validator/native-validator.directive.ts +++ b/projects/cdk/directives/native-validator/native-validator.directive.ts @@ -1,21 +1,28 @@ -import type {OnDestroy} from '@angular/core'; -import {Directive, ElementRef, HostListener, inject, Input, NgZone} from '@angular/core'; +import { + DestroyRef, + Directive, + ElementRef, + HostListener, + inject, + Input, + NgZone, +} from '@angular/core'; import type {AbstractControl, Validator} from '@angular/forms'; import {NG_VALIDATORS} from '@angular/forms'; -import {tuiZonefree} from '@taiga-ui/cdk/observables'; +import {tuiTakeUntilDestroyed, tuiZonefree} from '@taiga-ui/cdk/observables'; import {tuiProvide} from '@taiga-ui/cdk/utils'; -import {ReplaySubject, takeUntil, timer} from 'rxjs'; +import {timer} from 'rxjs'; @Directive({ standalone: true, selector: '[tuiNativeValidator]', providers: [tuiProvide(NG_VALIDATORS, TuiNativeValidatorDirective, true)], }) -export class TuiNativeValidatorDirective implements Validator, OnDestroy { +export class TuiNativeValidatorDirective implements Validator { + private readonly destroyRef = inject(DestroyRef); private readonly zone = inject(NgZone); private readonly host: HTMLInputElement = inject(ElementRef).nativeElement; private control?: AbstractControl; - private readonly destroy$ = new ReplaySubject(1); @Input() public tuiNativeValidator = 'Invalid'; @@ -24,23 +31,12 @@ export class TuiNativeValidatorDirective implements Validator, OnDestroy { this.control = control; timer(0) - .pipe( - tuiZonefree(this.zone), - // NOTE: takeUntilDestroyed and DestroyRef doesn't work, - // NG0911: View has already been destroyed - // https://github.com/angular/angular/issues/54527 - takeUntil(this.destroy$), - ) + .pipe(tuiZonefree(this.zone), tuiTakeUntilDestroyed(this.destroyRef)) .subscribe(() => this.handleValidation()); return null; } - public ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - @HostListener('blur') protected handleValidation(): void { this.el.setCustomValidity?.( diff --git a/projects/cdk/observables/index.ts b/projects/cdk/observables/index.ts index 327b7efe82bc..86797e467eaa 100644 --- a/projects/cdk/observables/index.ts +++ b/projects/cdk/observables/index.ts @@ -11,6 +11,7 @@ export * from './pressed-observable'; export * from './prevent-default'; export * from './scroll-from'; export * from './stop-propagation'; +export * from './take-until-destroyed'; export * from './typed-from-event'; export * from './watch'; export * from './zone-free'; diff --git a/projects/cdk/observables/take-until-destroyed.ts b/projects/cdk/observables/take-until-destroyed.ts new file mode 100644 index 000000000000..ed817c73610c --- /dev/null +++ b/projects/cdk/observables/take-until-destroyed.ts @@ -0,0 +1,22 @@ +import type {DestroyRef} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import type {MonoTypeOperatorFunction} from 'rxjs'; +import {catchError, defaultIfEmpty, EMPTY, NEVER, pipe, takeUntil} from 'rxjs'; + +// NOTE: takeUntilDestroyed and DestroyRef can cause error: +// NG0911: View has already been destroyed +// https://github.com/angular/angular/issues/54527 +export function tuiTakeUntilDestroyed( + destroyRef?: DestroyRef, +): MonoTypeOperatorFunction { + return pipe( + takeUntil( + NEVER.pipe( + // eslint-disable-next-line rxjs/no-unsafe-takeuntil + takeUntilDestroyed(destroyRef), + catchError(() => EMPTY), + defaultIfEmpty(null), + ), + ), + ); +} diff --git a/projects/core/services/position.service.ts b/projects/core/services/position.service.ts index 8ff6feca9183..e8b0bfdc0a4c 100644 --- a/projects/core/services/position.service.ts +++ b/projects/core/services/position.service.ts @@ -12,14 +12,14 @@ export class TuiPositionService extends Observable { constructor() { const animationFrame$ = inject(ANIMATION_FRAME); - const destroy$ = inject(NgZone); + const zone = inject(NgZone); super(subscriber => animationFrame$ .pipe( map(() => this.el.getBoundingClientRect()), map(rect => this.accessor.getPosition(rect)), - tuiZonefree(destroy$), + tuiZonefree(zone), finalize(() => this.accessor.getPosition(EMPTY_CLIENT_RECT)), ) .subscribe(subscriber), diff --git a/projects/demo/src/modules/directives/appearance/appearance.template.html b/projects/demo/src/modules/directives/appearance/appearance.template.html index d5f28150483a..503d1d41393f 100644 --- a/projects/demo/src/modules/directives/appearance/appearance.template.html +++ b/projects/demo/src/modules/directives/appearance/appearance.template.html @@ -38,7 +38,7 @@