diff --git a/goldens/public-api/forms/forms.md b/goldens/public-api/forms/forms.md index 64357af46a5..eaa76df8e88 100644 --- a/goldens/public-api/forms/forms.md +++ b/goldens/public-api/forms/forms.md @@ -276,7 +276,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy // @public export class FormBuilder { array(controlsConfig: any[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray; - control(formState: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl; + control(formState: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl; group(controlsConfig: { [key: string]: any; }, options?: AbstractControlOptions | null): FormGroup; @@ -294,7 +294,8 @@ export class FormBuilder { // @public export class FormControl extends AbstractControl { - constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + readonly defaultValue: any; patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -361,6 +362,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export interface FormControlOptions extends AbstractControlOptions { + initialValueIsDefault?: boolean; +} + // @public export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index f6e18484f60..817d14f52e1 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -10,7 +10,7 @@ import {Injectable} from '@angular/core'; import {AsyncValidatorFn, ValidatorFn} from './directives/validators'; import {ReactiveFormsModule} from './form_providers'; -import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup, FormHooks} from './model'; +import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormGroup, FormHooks} from './model'; function isAbstractControlOptions(options: AbstractControlOptions| {[key: string]: any}): options is AbstractControlOptions { @@ -127,7 +127,7 @@ export class FormBuilder { * */ control( - formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl { return new FormControl(formState, validatorOrOpts, asyncValidator); } diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 08eff0b685d..326b9621c28 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -43,7 +43,7 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor'; export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; export {FormBuilder} from './form_builder'; -export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlStatus, FormGroup} from './model'; +export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormControlStatus, FormGroup} from './model'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; export {VERSION} from './version'; diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 8d5620f33f6..9ae55af2f7c 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -146,6 +146,25 @@ export interface AbstractControlOptions { updateOn?: 'change'|'blur'|'submit'; } +/** + * Interface for options provided to a {@link FormControl}. + * + * This interface extends all options from {@link AbstractControlOptions}, plus some options + * unique to `FormControl`. + * + * @publicApi + */ +export interface FormControlOptions extends AbstractControlOptions { + /** + * @description + * Whether to use the initial value used to construct the FormControl as its default value as + * well. If this option is false or not provided, the default value of a FormControl is `null`. + * When a FormControl is {@link FormControl#reset} without an explicit value, its value reverts to + * its default value. + */ + initialValueIsDefault?: boolean; +} + function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| null): validatorOrOpts is AbstractControlOptions { return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && @@ -1228,10 +1247,17 @@ export abstract class AbstractControl { * @publicApi */ export class FormControl extends AbstractControl { + /** + * The default value of this FormControl, used whenever the control is reset without an explicit + * value. See {@link FormControlOptions#initialValueIsDefault} for more information on configuring + * a default value. + * @publicApi + */ + public readonly defaultValue: any = null; + /** @internal */ _onChange: Array = []; - /** * This field holds a pending value that has not yet been applied to the form's value. * It is `any` because the value is untyped. @@ -1256,8 +1282,7 @@ export class FormControl extends AbstractControl { * */ constructor( - formState: any = null, - validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + formState: any = null, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); this._applyFormState(formState); @@ -1271,6 +1296,13 @@ export class FormControl extends AbstractControl { // to `true` to allow that during the control creation process. emitEvent: !!this.asyncValidator }); + if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) { + if (this._isBoxedValue(formState)) { + this.defaultValue = formState.value; + } else { + this.defaultValue = formState; + } + } } /** @@ -1329,8 +1361,23 @@ export class FormControl extends AbstractControl { } /** - * Resets the form control, marking it `pristine` and `untouched`, and setting - * the value to null. + * Resets the form control, marking it `pristine` and `untouched`, and resetting + * the value. The new value will be the provided value (if passed), `null`, or the initial value + * if `initialValueIsDefault` was set in the constructor via {@link FormControlOptions}. + * + * ```ts + * // By default, the control will reset to null. + * const dog = new FormControl('spot'); + * dog.reset(); // dog.value is null + * + * // If this flag is set, the control will instead reset to the initial value. + * const cat = new FormControl('tabby', {initialValueIsDefault: true}); + * cat.reset(); // cat.value is "tabby" + * + * // A value passed to reset always takes precedence. + * const fish = new FormControl('finn', {initialValueIsDefault: true}); + * fish.reset('bubble'); // fish.value is "bubble" + * ``` * * @param formState Resets the control with an initial value, * or an object that defines the initial value and disabled state. @@ -1346,8 +1393,10 @@ export class FormControl extends AbstractControl { * When false, no events are emitted. * */ - override reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): - void { + override reset(formState: any = this.defaultValue, options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { this._applyFormState(formState); this.markAsPristine(options); this.markAsUntouched(options); diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 3d1faf3bd4e..1f880cbacb6 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -817,6 +817,62 @@ describe('FormControl', () => { expect(c.value).toBe(null); }); + it('should reset to the initial value if specified in FormControlOptions', () => { + const c2 = new FormControl('foo', {initialValueIsDefault: true}); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + + c2.setValue('bar'); + expect(c2.value).toBe('bar'); + expect(c2.defaultValue).toBe('foo'); + + c2.reset(); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + + const c3 = new FormControl('foo', {initialValueIsDefault: false}); + expect(c3.value).toBe('foo'); + expect(c3.defaultValue).toBe(null); + + c3.setValue('bar'); + expect(c3.value).toBe('bar'); + expect(c3.defaultValue).toBe(null); + + c3.reset(); + expect(c3.value).toBe(null); + expect(c3.defaultValue).toBe(null); + }); + + it('should look inside FormState objects for a default value', () => { + const c2 = new FormControl({value: 'foo', disabled: false}, {initialValueIsDefault: true}); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + + c2.setValue('bar'); + expect(c2.value).toBe('bar'); + expect(c2.defaultValue).toBe('foo'); + + c2.reset(); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + }); + + it('should not alter the disabled state when resetting, even if a default value is provided', + () => { + const c2 = new FormControl({value: 'foo', disabled: true}, {initialValueIsDefault: true}); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + expect(c2.disabled).toBe(true); + + c2.setValue('bar'); + c2.enable(); + + c2.reset(); + expect(c2.value).toBe('foo'); + expect(c2.defaultValue).toBe('foo'); + expect(c2.disabled).toBe(false); + }); + it('should update the value of any parent controls with passed value', () => { const g = new FormGroup({'one': c}); c.setValue('new value');