From 953989cda1a1792a9fc58070c55e0557c0d3ce28 Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Tue, 30 Nov 2021 14:46:46 -0500 Subject: [PATCH] refactor(forms): Move FormControl to an overridden constructor. This implementation change was originally proposed as part of Typed Forms, and will have major consequences for that project as described in the design doc. Submitting it separately will greatly simplify the risk of landing Typed Forms. This change should have no visible impact on normal users of FormControl. See the Typed Forms design doc here: https://docs.google.com/document/d/1cWuBE-oo5WLtwkLFxbNTiaVQGNk8ipgbekZcKBeyxxo. --- goldens/public-api/forms/forms.md | 6 +- packages/forms/src/model.ts | 156 ++++++++++++++++++++++- packages/forms/test/form_control_spec.ts | 21 ++- 3 files changed, 175 insertions(+), 8 deletions(-) diff --git a/goldens/public-api/forms/forms.md b/goldens/public-api/forms/forms.md index 64357af46a53d5..0a2f8b0f5eca67 100644 --- a/goldens/public-api/forms/forms.md +++ b/goldens/public-api/forms/forms.md @@ -293,8 +293,7 @@ export class FormBuilder { } // @public -export class FormControl extends AbstractControl { - constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); +export interface FormControl extends AbstractControl { patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -315,6 +314,9 @@ export class FormControl extends AbstractControl { }): void; } +// @public (undocumented) +export const FormControl: FormControlCtor; + // @public export class FormControlDirective extends NgControl implements OnChanges, OnDestroy { constructor(validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null); diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 558321bc54d417..b19328f705e38f 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -1118,7 +1118,6 @@ export abstract class AbstractControl { this._updateOn = opts.updateOn!; } } - /** * Check to see if parent has been marked artificially dirty. * @@ -1144,6 +1143,14 @@ export abstract class AbstractControl { * * @usageNotes * + * ### Available Constructors + * + * ```ts + * new(): FormControl; + * new(value: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + * asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; + * ``` + * * ### Initializing Form Controls * * Instantiate a `FormControl`, with an initial value. @@ -1151,7 +1158,7 @@ export abstract class AbstractControl { * ```ts * const control = new FormControl('some value'); * console.log(control.value); // 'some value' - *``` + * ``` * * The following example initializes the control with a form state object. The `value` * and `disabled` keys are required in this case. @@ -1227,15 +1234,150 @@ export abstract class AbstractControl { * * @publicApi */ -export class FormControl extends AbstractControl { +export declare interface FormControl extends AbstractControl { + /** @internal */ + _onChange: Function[]; + + /** @internal */ + _pendingValue: boolean; + + /** @internal */ + _pendingChange: boolean; + + /** + * Sets a new value for the form control. + * + * @param value The new value for the control. + * @param options Configuration options that determine how the control propagates changes + * and emits events when the value changes. + * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity + * updateValueAndValidity} method. + * + * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is + * false. + * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and + * `valueChanges` + * observables emit events with the latest status and value when the control value is updated. + * When false, no events are emitted. + * * `emitModelToViewChange`: When true or not supplied (the default), each change triggers an + * `onChange` event to + * update the view. + * * `emitViewToModelChange`: When true or not supplied (the default), each change triggers an + * `ngModelChange` + * event to update the model. + * + */ + setValue(value: any, options?: { + onlySelf?: boolean, + emitEvent?: boolean, + emitModelToViewChange?: boolean, + emitViewToModelChange?: boolean + }): void; + + /** + * Patches the value of a control. + * + * This function is functionally the same as {@link FormControl#setValue setValue} at this level. + * It exists for symmetry with {@link FormGroup#patchValue patchValue} on `FormGroups` and + * `FormArrays`, where it does behave differently. + * + * @see `setValue` for options + */ + patchValue(value: any, options?: { + onlySelf?: boolean, + emitEvent?: boolean, + emitModelToViewChange?: boolean, + emitViewToModelChange?: boolean + }): void; + + /** + * Resets the form control, marking it `pristine` and `untouched`, and setting + * the value to the provided default, or null if no value is provided. + * + * @param formState Resets the control with an initial value, + * or an object that defines the initial value and disabled state. + * + * @param options Configuration options that determine how the control propagates changes + * and emits events after the value changes. + * + * * `onlySelf`: When true, each change only affects this control, and not its parent. Default is + * false. + * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and + * `valueChanges` + * observables emit events with the latest status and value when the control is reset. + * When false, no events are emitted. + * + */ + reset(formState?: any, options?: {onlySelf?: boolean, emitEvent?: boolean}): void; + + /** + * @internal + */ + _updateValue(): void; + + /** + * @internal + */ + _anyControls(condition: Function): boolean; + + /** + * @internal + */ + _allControlsDisabled(): boolean; + + /** + * Register a listener for change events. + * + * @param fn The method that is called when the value changes + */ + registerOnChange(fn: Function): void; + + /** + * Internal function to unregister a change events listener. + * @internal + */ + _unregisterOnChange(fn: Function): void; + + /** + * Register a listener for disabled events. + * + * @param fn The method that is called when the disabled status changes. + */ + registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; + + /** + * Internal function to unregister a disabled event listener. + * @internal + */ + _unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void; + + /** + * @internal + */ + _forEachChild(cb: Function): void; + + /** @internal */ + _syncPendingControls(): boolean; +} + +/** + * @publicApi + */ +export declare interface FormControlCtor { + new(): FormControl; + new(value: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; +} + +export class FormControlImpl extends AbstractControl { /** @internal */ _onChange: Function[] = []; /** @internal */ - _pendingValue: any; + _pendingValue: boolean = false; /** @internal */ - _pendingChange: any; + _pendingChange: boolean = false; /** * Creates a new `FormControl` instance. @@ -1432,6 +1574,10 @@ export class FormControl extends AbstractControl { } } +// The constructor for FormControl is decoupled from its implementation. +// This allows us to provide multiple constructor signatures. +export const FormControl: FormControlCtor = FormControlImpl as FormControlCtor; + /** * Tracks the value and validity state of a group of `FormControl` instances. * diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 0c8900a4d0bcd0..f0e664b0aa4bfc 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -8,8 +8,8 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {FormControl, FormGroup, Validators} from '@angular/forms'; - import {FormArray} from '@angular/forms/src/model'; + import {asyncValidator, asyncValidatorReturningObservable} from './util'; (function() { @@ -1469,5 +1469,24 @@ describe('FormControl', () => { }); }); }); + + describe('can be extended', () => { + // We don't technically support extending Forms classes, but people do it anyway. + // We need to make sure that there is some way to extend them to avoid causing breakage. + + class FCExt extends FormControl { + constructor(formState?: any|{ + value?: any; + disabled?: boolean; + }, ...args: any) { + super(formState, ...args); + } + } + + it('should perform basic FormControl operations', () => { + const nc = new FCExt({value: 'foo'}); + nc.setValue('bar'); + }); + }); }); })();