From 2d2fe33b135168a515abe3d41a86a0f2ba9ddfcf Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Tue, 27 Sep 2022 12:28:34 +0100 Subject: [PATCH] fix(module:typography): focus the element and set the value even if the zone is already stable (#7320) --- components/typography/text-edit.component.ts | 61 ++++++++++---------- components/typography/typography.spec.ts | 11 ++-- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/components/typography/text-edit.component.ts b/components/typography/text-edit.component.ts index c139903676..c0adad7632 100644 --- a/components/typography/text-edit.component.ts +++ b/components/typography/text-edit.component.ts @@ -17,8 +17,8 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { BehaviorSubject, fromEvent, Observable } from 'rxjs'; -import { filter, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators'; +import { BehaviorSubject, EMPTY, from, fromEvent, Observable } from 'rxjs'; +import { switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; import { NzTSType } from 'ng-zorro-antd/core/types'; @@ -66,9 +66,7 @@ export class NzTextEditComponent implements OnInit { @Output() readonly endEditing = new EventEmitter(true); @ViewChild('textarea', { static: false }) set textarea(textarea: ElementRef | undefined) { - if (textarea) { - this.textarea$.next(textarea); - } + this.textarea$.next(textarea); } @ViewChild(NzAutosizeDirective, { static: false }) autosizeDirective!: NzAutosizeDirective; @@ -79,7 +77,7 @@ export class NzTextEditComponent implements OnInit { // We could've saved the textarea within some private property (e.g. `_textarea`) and have a getter, // but having subject makes the code more reactive and cancellable (e.g. event listeners will be // automatically removed and re-added through the `switchMap` below). - private textarea$ = new BehaviorSubject | null>(null); + private textarea$ = new BehaviorSubject | null | undefined>(null); constructor( private ngZone: NgZone, @@ -95,22 +93,19 @@ export class NzTextEditComponent implements OnInit { this.cdr.markForCheck(); }); - const textarea$: Observable> = this.textarea$.pipe( - filter((textarea): textarea is ElementRef => textarea !== null) - ); - - textarea$ + this.textarea$ .pipe( - switchMap( - textarea => - // Caretaker note: we explicitly should call `subscribe()` within the root zone. - // `runOutsideAngular(() => fromEvent(...))` will just create an observable within the root zone, - // but `addEventListener` is called when the `fromEvent` is subscribed. - new Observable(subscriber => - this.ngZone.runOutsideAngular(() => - fromEvent(textarea.nativeElement, 'keydown').subscribe(subscriber) + switchMap(textarea => + // Caretaker note: we explicitly should call `subscribe()` within the root zone. + // `runOutsideAngular(() => fromEvent(...))` will just create an observable within the root zone, + // but `addEventListener` is called when the `fromEvent` is subscribed. + textarea + ? new Observable(subscriber => + this.ngZone.runOutsideAngular(() => + fromEvent(textarea.nativeElement, 'keydown').subscribe(subscriber) + ) ) - ) + : EMPTY ), takeUntil(this.destroy$) ) @@ -133,15 +128,16 @@ export class NzTextEditComponent implements OnInit { }); }); - textarea$ + this.textarea$ .pipe( - switchMap( - textarea => - new Observable(subscriber => - this.ngZone.runOutsideAngular(() => - fromEvent(textarea.nativeElement, 'input').subscribe(subscriber) + switchMap(textarea => + textarea + ? new Observable(subscriber => + this.ngZone.runOutsideAngular(() => + fromEvent(textarea.nativeElement, 'input').subscribe(subscriber) + ) ) - ) + : EMPTY ), takeUntil(this.destroy$) ) @@ -175,9 +171,13 @@ export class NzTextEditComponent implements OnInit { } focusAndSetValue(): void { - this.ngZone.onStable - .pipe(take(1), withLatestFrom(this.textarea$), takeUntil(this.destroy$)) - .subscribe(([, textarea]) => { + // Note: the zone may be nooped through `BootstrapOptions` when bootstrapping the root module. This means + // the `onStable` will never emit any value. + const onStable$ = this.ngZone.isStable ? from(Promise.resolve()) : this.ngZone.onStable.pipe(take(1)); + // Normally this isn't in the zone, but it can cause performance regressions for apps + // using `zone-patch-rxjs` because it'll trigger a change detection when it unsubscribes. + this.ngZone.runOutsideAngular(() => { + onStable$.pipe(withLatestFrom(this.textarea$), takeUntil(this.destroy$)).subscribe(([, textarea]) => { if (textarea) { textarea.nativeElement.focus(); textarea.nativeElement.value = this.currentText || ''; @@ -185,5 +185,6 @@ export class NzTextEditComponent implements OnInit { this.cdr.markForCheck(); } }); + }); } } diff --git a/components/typography/typography.spec.ts b/components/typography/typography.spec.ts index a6c4fd425d..9cf4204df7 100644 --- a/components/typography/typography.spec.ts +++ b/components/typography/typography.spec.ts @@ -2,7 +2,7 @@ import { CAPS_LOCK, ENTER, ESCAPE, TAB } from '@angular/cdk/keycodes'; import { OverlayContainer } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { ApplicationRef, Component, NgZone, ViewChild } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, flush, flushMicrotasks, inject, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -28,12 +28,11 @@ describe('typography', () => { let componentElement: HTMLElement; let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; - let zone: MockNgZone; beforeEach(() => { TestBed.configureTestingModule({ imports: [CommonModule, NzTypographyModule, NzIconTestModule, NoopAnimationsModule], - providers: [{ provide: NgZone, useFactory: () => (zone = new MockNgZone()) }], + providers: [{ provide: NgZone, useFactory: () => new MockNgZone() }], declarations: [ NzTestTypographyComponent, NzTestTypographyCopyComponent, @@ -283,9 +282,11 @@ describe('typography', () => { it('should edit focus', fakeAsync(() => { const editButton = componentElement.querySelector('.ant-typography-edit'); editButton!.click(); - fixture.detectChanges(); - zone.simulateZoneExit(); + // The zone may be already stable (see `isStable` condition), thus there're no tasks + // in the queue that have been scheduled previously. + // This will schedule a microtask (except of waiting for `onStable`). + flushMicrotasks(); const textarea = componentElement.querySelector('textarea')! as HTMLTextAreaElement;