From 4456905d9cfcc3535ddcd6dc09017b5850168e97 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 7 May 2024 15:00:10 -0700 Subject: [PATCH] fix(forms): Make `NgControlStatus` host bindings `OnPush` compatible This commit makes the host bindings of `NgControlStatus[Group]` compatible with `OnPush` components. Note that this intentionally _does not_ expose any new APIs in the forms module. The goal is only to remove unpreventable `ExpressionChangedAfterItHasBeenCheckedError` in the forms code that developers do not have control over. --- .../forms/src/directives/ng_control_status.ts | 11 ++++++++ packages/forms/src/directives/ng_form.ts | 5 ++++ .../form_group_directive.ts | 9 ++++-- packages/forms/src/model/abstract_model.ts | 28 ++++++++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/forms/src/directives/ng_control_status.ts b/packages/forms/src/directives/ng_control_status.ts index ca9a9b2c5a2b82..a9de3185aa5cd7 100644 --- a/packages/forms/src/directives/ng_control_status.ts +++ b/packages/forms/src/directives/ng_control_status.ts @@ -26,6 +26,8 @@ export class AbstractControlStatus { } protected get isTouched() { + // track the touched signal + this._cd?.control?._touched?.(); return !!this._cd?.control?.touched; } @@ -34,26 +36,35 @@ export class AbstractControlStatus { } protected get isPristine() { + // track the pristine signal + this._cd?.control?._pristine?.(); return !!this._cd?.control?.pristine; } protected get isDirty() { + // pristine signal already tracked above return !!this._cd?.control?.dirty; } protected get isValid() { + // track the status signal + this._cd?.control?._status?.(); return !!this._cd?.control?.valid; } protected get isInvalid() { + // status signal already tracked above return !!this._cd?.control?.invalid; } protected get isPending() { + // status signal already tracked above return !!this._cd?.control?.pending; } protected get isSubmitted() { + // track the submitted signal + (this._cd as Writable | null)?._submitted?.(); // We check for the `submitted` field from `NgForm` and `FormGroupDirective` classes, but // we avoid instanceof checks to prevent non-tree-shakable references to those types. return !!(this._cd as Writable | null)?.submitted; diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index aa759817653bfc..4d740c764b65fe 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -16,6 +16,7 @@ import { Optional, Provider, Self, + signal, ɵWritable as Writable, } from '@angular/core'; @@ -127,6 +128,8 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit { * Returns whether the form submission has been triggered. */ public readonly submitted: boolean = false; + /** @internal */ + readonly _submitted = signal(false); private _directives = new Set(); @@ -328,6 +331,7 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit { */ onSubmit($event: Event): boolean { (this as Writable).submitted = true; + this._submitted.set(true); syncPendingControls(this.form, this._directives); this.ngSubmit.emit($event); // Forms with `method="dialog"` have some special behavior @@ -352,6 +356,7 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit { resetForm(value: any = undefined): void { this.form.reset(value); (this as Writable).submitted = false; + this._submitted.set(false); } private _setUpdateStrategy() { diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 835191c914e597..211892aacab358 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -18,6 +18,7 @@ import { Output, Provider, Self, + signal, SimpleChanges, ɵWritable as Writable, } from '@angular/core'; @@ -87,10 +88,12 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan * Reports whether the form submission has been triggered. */ public readonly submitted: boolean = false; + /** @internal */ + readonly _submitted = signal(false); /** - * Reference to an old form group input value, which is needed to cleanup old instance in case it - * was replaced with a new one. + * Reference to an old form group input value, which is needed to cleanup + * old instance in case it was replaced with a new one. */ private _oldForm: FormGroup | undefined; @@ -300,6 +303,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan */ onSubmit($event: Event): boolean { (this as Writable).submitted = true; + this._submitted.set(true); syncPendingControls(this.form, this.directives); this.ngSubmit.emit($event); // Forms with `method="dialog"` have some special behavior that won't reload the page and that @@ -325,6 +329,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan resetForm(value: any = undefined): void { this.form.reset(value); (this as Writable).submitted = false; + this._submitted.set(false); } /** @internal */ diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index 1c185c7c2f04d2..ab6d4f26ca98f6 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {EventEmitter, ɵRuntimeError as RuntimeError, ɵWritable as Writable} from '@angular/core'; +import { + EventEmitter, + signal, + ɵRuntimeError as RuntimeError, + ɵWritable as Writable, +} from '@angular/core'; import {Observable, Subject} from 'rxjs'; import { @@ -569,6 +574,8 @@ export abstract class AbstractControl(null); /** * A control is `valid` when its `status` is `VALID`. @@ -649,6 +656,9 @@ export abstract class AbstractControl).touched = true; + this._touched.set(this.touched); const sourceControl = opts.sourceControl ?? this; if (this._parent && !opts.onlySelf) { @@ -993,6 +1007,7 @@ export abstract class AbstractControl).touched = false; + this._touched.set(this.touched); this._pendingTouched = false; const sourceControl = opts.sourceControl ?? this; @@ -1039,6 +1054,7 @@ export abstract class AbstractControl).pristine = false; + this._pristine.set(false); const sourceControl = opts.sourceControl ?? this; if (this._parent && !opts.onlySelf) { @@ -1083,6 +1099,7 @@ export abstract class AbstractControl).pristine = true; + this._pristine.set(true); this._pendingDirty = false; const sourceControl = opts.sourceControl ?? this; @@ -1130,6 +1147,7 @@ export abstract class AbstractControl).status = PENDING; + this._status.set(PENDING); const sourceControl = opts.sourceControl ?? this; if (opts.emitEvent !== false) { @@ -1172,6 +1190,7 @@ export abstract class AbstractControl).status = DISABLED; + this._status.set(DISABLED); (this as Writable).errors = null; this._forEachChild((control: AbstractControl) => { /** We don't propagate the source control downwards */ @@ -1215,6 +1234,7 @@ export abstract class AbstractControl).status = VALID; + this._status.set(VALID); this._forEachChild((control: AbstractControl) => { control.enable({...opts, onlySelf: true}); }); @@ -1302,6 +1322,7 @@ export abstract class AbstractControl).errors = this._runValidator(); (this as Writable).status = this._calculateStatus(); + this._status.set(this.status); if (this.status === VALID || this.status === PENDING) { this._runAsyncValidator(opts.emitEvent); @@ -1329,6 +1350,7 @@ export abstract class AbstractControl).status = this._allControlsDisabled() ? DISABLED : VALID; + this._status.set(this.status); } private _runValidator(): ValidationErrors | null { @@ -1338,6 +1360,7 @@ export abstract class AbstractControl).status = PENDING; + this._status.set(PENDING); this._hasOwnPendingAsyncValidator = true; const obs = toObservable(this.asyncValidator(this)); this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors | null) => { @@ -1534,6 +1557,7 @@ export abstract class AbstractControl).status = this._calculateStatus(); + this._status.set(this.status); if (emitEvent) { (this.statusChanges as EventEmitter).emit(this.status); @@ -1594,6 +1618,7 @@ export abstract class AbstractControl).pristine = newPristine; + this._pristine.set(newPristine); if (this._parent && !opts.onlySelf) { this._parent._updatePristine(opts, changedControl); @@ -1607,6 +1632,7 @@ export abstract class AbstractControl).touched = this._anyControlsTouched(); + this._touched.set(this.touched); this._events.next(new TouchedChangeEvent(this.touched, changedControl)); if (this._parent && !opts.onlySelf) {