Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(module:typography): focus the element and set the value even if the zone is already stable #7320

Merged
merged 1 commit into from Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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