Skip to content

Commit

Permalink
fix(module:typography): focus the element and set the value even if t…
Browse files Browse the repository at this point in the history
…he zone is already stable (#7320)
  • Loading branch information
arturovt committed Sep 27, 2022
1 parent 82159e3 commit 2d2fe33
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 35 deletions.
61 changes: 31 additions & 30 deletions components/typography/text-edit.component.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -66,9 +66,7 @@ export class NzTextEditComponent implements OnInit {
@Output() readonly endEditing = new EventEmitter<string>(true);
@ViewChild('textarea', { static: false })
set textarea(textarea: ElementRef<HTMLTextAreaElement> | undefined) {
if (textarea) {
this.textarea$.next(textarea);
}
this.textarea$.next(textarea);
}
@ViewChild(NzAutosizeDirective, { static: false }) autosizeDirective!: NzAutosizeDirective;

Expand All @@ -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<ElementRef<HTMLTextAreaElement> | null>(null);
private textarea$ = new BehaviorSubject<ElementRef<HTMLTextAreaElement> | null | undefined>(null);

constructor(
private ngZone: NgZone,
Expand All @@ -95,22 +93,19 @@ export class NzTextEditComponent implements OnInit {
this.cdr.markForCheck();
});

const textarea$: Observable<ElementRef<HTMLTextAreaElement>> = this.textarea$.pipe(
filter((textarea): textarea is ElementRef<HTMLTextAreaElement> => 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<KeyboardEvent>(subscriber =>
this.ngZone.runOutsideAngular(() =>
fromEvent<KeyboardEvent>(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<KeyboardEvent>(subscriber =>
this.ngZone.runOutsideAngular(() =>
fromEvent<KeyboardEvent>(textarea.nativeElement, 'keydown').subscribe(subscriber)
)
)
)
: EMPTY
),
takeUntil(this.destroy$)
)
Expand All @@ -133,15 +128,16 @@ export class NzTextEditComponent implements OnInit {
});
});

textarea$
this.textarea$
.pipe(
switchMap(
textarea =>
new Observable<KeyboardEvent>(subscriber =>
this.ngZone.runOutsideAngular(() =>
fromEvent<KeyboardEvent>(textarea.nativeElement, 'input').subscribe(subscriber)
switchMap(textarea =>
textarea
? new Observable<KeyboardEvent>(subscriber =>
this.ngZone.runOutsideAngular(() =>
fromEvent<KeyboardEvent>(textarea.nativeElement, 'input').subscribe(subscriber)
)
)
)
: EMPTY
),
takeUntil(this.destroy$)
)
Expand Down Expand Up @@ -175,15 +171,20 @@ 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 || '';
this.autosizeDirective.resizeToFitContent();
this.cdr.markForCheck();
}
});
});
}
}
11 changes: 6 additions & 5 deletions components/typography/typography.spec.ts
Expand Up @@ -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';

Expand All @@ -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,
Expand Down Expand Up @@ -283,9 +282,11 @@ describe('typography', () => {
it('should edit focus', fakeAsync(() => {
const editButton = componentElement.querySelector<HTMLButtonElement>('.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<HTMLTextAreaElement>('textarea')! as HTMLTextAreaElement;

Expand Down

0 comments on commit 2d2fe33

Please sign in to comment.