Skip to content

Commit 2d2fe33

Browse files
authored
fix(module:typography): focus the element and set the value even if the zone is already stable (#7320)
1 parent 82159e3 commit 2d2fe33

2 files changed

Lines changed: 37 additions & 35 deletions

File tree

components/typography/text-edit.component.ts

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
ViewChild,
1818
ViewEncapsulation
1919
} from '@angular/core';
20-
import { BehaviorSubject, fromEvent, Observable } from 'rxjs';
21-
import { filter, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators';
20+
import { BehaviorSubject, EMPTY, from, fromEvent, Observable } from 'rxjs';
21+
import { switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators';
2222

2323
import { NzDestroyService } from 'ng-zorro-antd/core/services';
2424
import { NzTSType } from 'ng-zorro-antd/core/types';
@@ -66,9 +66,7 @@ export class NzTextEditComponent implements OnInit {
6666
@Output() readonly endEditing = new EventEmitter<string>(true);
6767
@ViewChild('textarea', { static: false })
6868
set textarea(textarea: ElementRef<HTMLTextAreaElement> | undefined) {
69-
if (textarea) {
70-
this.textarea$.next(textarea);
71-
}
69+
this.textarea$.next(textarea);
7270
}
7371
@ViewChild(NzAutosizeDirective, { static: false }) autosizeDirective!: NzAutosizeDirective;
7472

@@ -79,7 +77,7 @@ export class NzTextEditComponent implements OnInit {
7977
// We could've saved the textarea within some private property (e.g. `_textarea`) and have a getter,
8078
// but having subject makes the code more reactive and cancellable (e.g. event listeners will be
8179
// automatically removed and re-added through the `switchMap` below).
82-
private textarea$ = new BehaviorSubject<ElementRef<HTMLTextAreaElement> | null>(null);
80+
private textarea$ = new BehaviorSubject<ElementRef<HTMLTextAreaElement> | null | undefined>(null);
8381

8482
constructor(
8583
private ngZone: NgZone,
@@ -95,22 +93,19 @@ export class NzTextEditComponent implements OnInit {
9593
this.cdr.markForCheck();
9694
});
9795

98-
const textarea$: Observable<ElementRef<HTMLTextAreaElement>> = this.textarea$.pipe(
99-
filter((textarea): textarea is ElementRef<HTMLTextAreaElement> => textarea !== null)
100-
);
101-
102-
textarea$
96+
this.textarea$
10397
.pipe(
104-
switchMap(
105-
textarea =>
106-
// Caretaker note: we explicitly should call `subscribe()` within the root zone.
107-
// `runOutsideAngular(() => fromEvent(...))` will just create an observable within the root zone,
108-
// but `addEventListener` is called when the `fromEvent` is subscribed.
109-
new Observable<KeyboardEvent>(subscriber =>
110-
this.ngZone.runOutsideAngular(() =>
111-
fromEvent<KeyboardEvent>(textarea.nativeElement, 'keydown').subscribe(subscriber)
98+
switchMap(textarea =>
99+
// Caretaker note: we explicitly should call `subscribe()` within the root zone.
100+
// `runOutsideAngular(() => fromEvent(...))` will just create an observable within the root zone,
101+
// but `addEventListener` is called when the `fromEvent` is subscribed.
102+
textarea
103+
? new Observable<KeyboardEvent>(subscriber =>
104+
this.ngZone.runOutsideAngular(() =>
105+
fromEvent<KeyboardEvent>(textarea.nativeElement, 'keydown').subscribe(subscriber)
106+
)
112107
)
113-
)
108+
: EMPTY
114109
),
115110
takeUntil(this.destroy$)
116111
)
@@ -133,15 +128,16 @@ export class NzTextEditComponent implements OnInit {
133128
});
134129
});
135130

136-
textarea$
131+
this.textarea$
137132
.pipe(
138-
switchMap(
139-
textarea =>
140-
new Observable<KeyboardEvent>(subscriber =>
141-
this.ngZone.runOutsideAngular(() =>
142-
fromEvent<KeyboardEvent>(textarea.nativeElement, 'input').subscribe(subscriber)
133+
switchMap(textarea =>
134+
textarea
135+
? new Observable<KeyboardEvent>(subscriber =>
136+
this.ngZone.runOutsideAngular(() =>
137+
fromEvent<KeyboardEvent>(textarea.nativeElement, 'input').subscribe(subscriber)
138+
)
143139
)
144-
)
140+
: EMPTY
145141
),
146142
takeUntil(this.destroy$)
147143
)
@@ -175,15 +171,20 @@ export class NzTextEditComponent implements OnInit {
175171
}
176172

177173
focusAndSetValue(): void {
178-
this.ngZone.onStable
179-
.pipe(take(1), withLatestFrom(this.textarea$), takeUntil(this.destroy$))
180-
.subscribe(([, textarea]) => {
174+
// Note: the zone may be nooped through `BootstrapOptions` when bootstrapping the root module. This means
175+
// the `onStable` will never emit any value.
176+
const onStable$ = this.ngZone.isStable ? from(Promise.resolve()) : this.ngZone.onStable.pipe(take(1));
177+
// Normally this isn't in the zone, but it can cause performance regressions for apps
178+
// using `zone-patch-rxjs` because it'll trigger a change detection when it unsubscribes.
179+
this.ngZone.runOutsideAngular(() => {
180+
onStable$.pipe(withLatestFrom(this.textarea$), takeUntil(this.destroy$)).subscribe(([, textarea]) => {
181181
if (textarea) {
182182
textarea.nativeElement.focus();
183183
textarea.nativeElement.value = this.currentText || '';
184184
this.autosizeDirective.resizeToFitContent();
185185
this.cdr.markForCheck();
186186
}
187187
});
188+
});
188189
}
189190
}

components/typography/typography.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CAPS_LOCK, ENTER, ESCAPE, TAB } from '@angular/cdk/keycodes';
22
import { OverlayContainer } from '@angular/cdk/overlay';
33
import { CommonModule } from '@angular/common';
44
import { ApplicationRef, Component, NgZone, ViewChild } from '@angular/core';
5-
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
5+
import { ComponentFixture, fakeAsync, flush, flushMicrotasks, inject, TestBed, tick } from '@angular/core/testing';
66
import { By } from '@angular/platform-browser';
77
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
88

@@ -28,12 +28,11 @@ describe('typography', () => {
2828
let componentElement: HTMLElement;
2929
let overlayContainer: OverlayContainer;
3030
let overlayContainerElement: HTMLElement;
31-
let zone: MockNgZone;
3231

3332
beforeEach(() => {
3433
TestBed.configureTestingModule({
3534
imports: [CommonModule, NzTypographyModule, NzIconTestModule, NoopAnimationsModule],
36-
providers: [{ provide: NgZone, useFactory: () => (zone = new MockNgZone()) }],
35+
providers: [{ provide: NgZone, useFactory: () => new MockNgZone() }],
3736
declarations: [
3837
NzTestTypographyComponent,
3938
NzTestTypographyCopyComponent,
@@ -283,9 +282,11 @@ describe('typography', () => {
283282
it('should edit focus', fakeAsync(() => {
284283
const editButton = componentElement.querySelector<HTMLButtonElement>('.ant-typography-edit');
285284
editButton!.click();
286-
287285
fixture.detectChanges();
288-
zone.simulateZoneExit();
286+
// The zone may be already stable (see `isStable` condition), thus there're no tasks
287+
// in the queue that have been scheduled previously.
288+
// This will schedule a microtask (except of waiting for `onStable`).
289+
flushMicrotasks();
289290

290291
const textarea = componentElement.querySelector<HTMLTextAreaElement>('textarea')! as HTMLTextAreaElement;
291292

0 commit comments

Comments
 (0)