From e49fc96ed33c26434a14b80487dd912d8c76cace Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Fri, 23 Jul 2021 14:35:08 -0700 Subject: [PATCH] feat(forms): Make Form Statuses use stricter types. (#42952) Specifically: narrow the type used for form statuses from string to a union of possible statuses. Change the API methods from any to use the new type. This is a breaking change. However, as discussed in the PR, breakage seems minimal, and google3 has been prepped to land this. Background: we uncovered these any typings in the course of design work for typed forms. They could be fixed in a non-breaking manner by piggybacking them on top of the new typed forms generics, but it would be much cleaner to fix them separately if possible. BREAKING CHANGE: A new type called `FormControlStatus` has been introduced, which is a union of all possible status strings for form controls. `AbstractControl.status` has been narrowed from `string` to `FormControlStatus`, and `statusChanges` has been narrowed from `Observable` to `Observable`. Most applications should consume the new types seamlessly. Any breakage caused by this change is likely due to one of the following two problems: (1) the app is comparing `AbstractControl.status` against a string which is not a valid status; or, (2) the app is using `statusChanges` events as if they were something other than strings. PR Close #42952 --- goldens/public-api/forms/forms.md | 4 +-- packages/forms/src/model.ts | 58 +++++++++++++++++++------------ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/goldens/public-api/forms/forms.md b/goldens/public-api/forms/forms.md index 332b1ddc0e..b9339e221f 100644 --- a/goldens/public-api/forms/forms.md +++ b/goldens/public-api/forms/forms.md @@ -78,8 +78,8 @@ export abstract class AbstractControl { setParent(parent: FormGroup | FormArray): void; setValidators(validators: ValidatorFn | ValidatorFn[] | null): void; abstract setValue(value: any, options?: Object): void; - readonly status: string; - readonly statusChanges: Observable; + readonly status: FormControlStatus; + readonly statusChanges: Observable; readonly touched: boolean; get untouched(): boolean; get updateOn(): FormHooks; diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index ec29843288..c84ea4565b 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -45,6 +45,23 @@ export const PENDING = 'PENDING'; */ export const DISABLED = 'DISABLED'; +/** + * A form can have several different statuses. Each + * possible status is returned as a string literal. + * + * * **VALID**: Reports that a FormControl is valid, meaning that no errors exist in the input + * value. + * * **INVALID**: Reports that a FormControl is invalid, meaning that an error exists in the input + * value. + * * **PENDING**: Reports that a FormControl is pending, meaning that that async validation is + * occurring and errors are not yet available for the input value. + * * **DISABLED**: Reports that a FormControl is + * disabled, meaning that the control is exempt from ancestor calculations of validity or value. + * + * @publicApi + */ +export type FormControlStatus = 'VALID'|'INVALID'|'PENDING'|'DISABLED'; + function _find(control: AbstractControl, path: Array|string, delimiter: string) { if (path == null) return null; @@ -274,19 +291,15 @@ export abstract class AbstractControl { } /** - * The validation status of the control. There are four possible - * validation status values: + * The validation status of the control. * - * * **VALID**: This control has passed all validation checks. - * * **INVALID**: This control has failed at least one validation check. - * * **PENDING**: This control is in the midst of conducting a validation check. - * * **DISABLED**: This control is exempt from validation checks. + * @see `FormControlStatus` * * These status values are mutually exclusive, so a control cannot be * both valid AND invalid or invalid AND disabled. */ // TODO(issue/24571): remove '!'. - public readonly status!: string; + public readonly status!: FormControlStatus; /** * A control is `valid` when its `status` is `VALID`. @@ -409,11 +422,12 @@ export abstract class AbstractControl { * A multicasting observable that emits an event every time the validation `status` of the control * recalculates. * + * @see `FormControlStatus` * @see {@link AbstractControl.status} * */ // TODO(issue/24571): remove '!'. - public readonly statusChanges!: Observable; + public readonly statusChanges!: Observable; /** * Reports the update strategy of the `AbstractControl` (meaning @@ -687,10 +701,10 @@ export abstract class AbstractControl { * */ markAsPending(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - (this as {status: string}).status = PENDING; + (this as {status: FormControlStatus}).status = PENDING; if (opts.emitEvent !== false) { - (this.statusChanges as EventEmitter).emit(this.status); + (this.statusChanges as EventEmitter).emit(this.status); } if (this._parent && !opts.onlySelf) { @@ -720,7 +734,7 @@ export abstract class AbstractControl { // parent's dirtiness based on the children. const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); - (this as {status: string}).status = DISABLED; + (this as {status: FormControlStatus}).status = DISABLED; (this as {errors: ValidationErrors | null}).errors = null; this._forEachChild((control: AbstractControl) => { control.disable({...opts, onlySelf: true}); @@ -729,7 +743,7 @@ export abstract class AbstractControl { if (opts.emitEvent !== false) { (this.valueChanges as EventEmitter).emit(this.value); - (this.statusChanges as EventEmitter).emit(this.status); + (this.statusChanges as EventEmitter).emit(this.status); } this._updateAncestors({...opts, skipPristineCheck}); @@ -759,7 +773,7 @@ export abstract class AbstractControl { // parent's dirtiness based on the children. const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); - (this as {status: string}).status = VALID; + (this as {status: FormControlStatus}).status = VALID; this._forEachChild((control: AbstractControl) => { control.enable({...opts, onlySelf: true}); }); @@ -823,7 +837,7 @@ export abstract class AbstractControl { if (this.enabled) { this._cancelExistingSubscription(); (this as {errors: ValidationErrors | null}).errors = this._runValidator(); - (this as {status: string}).status = this._calculateStatus(); + (this as {status: FormControlStatus}).status = this._calculateStatus(); if (this.status === VALID || this.status === PENDING) { this._runAsyncValidator(opts.emitEvent); @@ -832,7 +846,7 @@ export abstract class AbstractControl { if (opts.emitEvent !== false) { (this.valueChanges as EventEmitter).emit(this.value); - (this.statusChanges as EventEmitter).emit(this.status); + (this.statusChanges as EventEmitter).emit(this.status); } if (this._parent && !opts.onlySelf) { @@ -847,7 +861,7 @@ export abstract class AbstractControl { } private _setInitialStatus() { - (this as {status: string}).status = this._allControlsDisabled() ? DISABLED : VALID; + (this as {status: FormControlStatus}).status = this._allControlsDisabled() ? DISABLED : VALID; } private _runValidator(): ValidationErrors|null { @@ -856,7 +870,7 @@ export abstract class AbstractControl { private _runAsyncValidator(emitEvent?: boolean): void { if (this.asyncValidator) { - (this as {status: string}).status = PENDING; + (this as {status: FormControlStatus}).status = PENDING; this._hasOwnPendingAsyncValidator = true; const obs = toObservable(this.asyncValidator(this)); this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors|null) => { @@ -1017,10 +1031,10 @@ export abstract class AbstractControl { /** @internal */ _updateControlsErrors(emitEvent: boolean): void { - (this as {status: string}).status = this._calculateStatus(); + (this as {status: FormControlStatus}).status = this._calculateStatus(); if (emitEvent) { - (this.statusChanges as EventEmitter).emit(this.status); + (this.statusChanges as EventEmitter).emit(this.status); } if (this._parent) { @@ -1031,11 +1045,11 @@ export abstract class AbstractControl { /** @internal */ _initObservables() { (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); - (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); + (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); } - private _calculateStatus(): string { + private _calculateStatus(): FormControlStatus { if (this._allControlsDisabled()) return DISABLED; if (this.errors) return INVALID; if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING; @@ -1059,7 +1073,7 @@ export abstract class AbstractControl { abstract _syncPendingControls(): boolean; /** @internal */ - _anyControlsHaveStatus(status: string): boolean { + _anyControlsHaveStatus(status: FormControlStatus): boolean { return this._anyControls((control: AbstractControl) => control.status === status); }