Skip to content

Commit 831a87f

Browse files
committed
feat(forms): Allow a FormControl to use initial value as default.
Allow a FormControl to be reset to its initial value. Provide this feature via a new option in a FormControlOptions interface, based on AbstractControlOptions. Also, expose the default value as part of the public API. This is part of a feature that has been requested elsewhere (e.g. in #19747). This was originally proposed as part of typed forms. As discussed in the GDE session (and after with akushnir/alxhub), it is likely better to just reuse the initial value rather than accepting an additional default. It is desirable to land this separately in order to reduce the scope of the typed forms PR, and make it a types-only change. Pertains to issue #13721.
1 parent 4aa6965 commit 831a87f

File tree

5 files changed

+123
-12
lines changed

5 files changed

+123
-12
lines changed

goldens/public-api/forms/forms.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
276276
// @public
277277
export class FormBuilder {
278278
array(controlsConfig: any[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray;
279-
control(formState: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl;
279+
control(formState: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl;
280280
group(controlsConfig: {
281281
[key: string]: any;
282282
}, options?: AbstractControlOptions | null): FormGroup;
@@ -294,7 +294,8 @@ export class FormBuilder {
294294

295295
// @public
296296
export class FormControl extends AbstractControl {
297-
constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
297+
constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
298+
readonly defaultValue: any;
298299
patchValue(value: any, options?: {
299300
onlySelf?: boolean;
300301
emitEvent?: boolean;
@@ -361,6 +362,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
361362
static ɵfac: i0.ɵɵFactoryDeclaration<FormControlName, [{ optional: true; host: true; skipSelf: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; }]>;
362363
}
363364

365+
// @public
366+
export interface FormControlOptions extends AbstractControlOptions {
367+
initialValueIsDefault?: boolean;
368+
}
369+
364370
// @public
365371
export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
366372

packages/forms/src/form_builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Injectable} from '@angular/core';
1010

1111
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
1212
import {ReactiveFormsModule} from './form_providers';
13-
import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup, FormHooks} from './model';
13+
import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormGroup, FormHooks} from './model';
1414

1515
function isAbstractControlOptions(options: AbstractControlOptions|
1616
{[key: string]: any}): options is AbstractControlOptions {
@@ -127,7 +127,7 @@ export class FormBuilder {
127127
* </code-example>
128128
*/
129129
control(
130-
formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
130+
formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
131131
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl {
132132
return new FormControl(formState, validatorOrOpts, asyncValidator);
133133
}

packages/forms/src/forms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co
4343
export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
4444
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
4545
export {FormBuilder} from './form_builder';
46-
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlStatus, FormGroup} from './model';
46+
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormControlStatus, FormGroup} from './model';
4747
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
4848
export {VERSION} from './version';
4949

packages/forms/src/model.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,25 @@ export interface AbstractControlOptions {
146146
updateOn?: 'change'|'blur'|'submit';
147147
}
148148

149+
/**
150+
* Interface for options provided to a {@link FormControl}.
151+
*
152+
* This interface extends all options from {@link AbstractControlOptions}, plus some options
153+
* unique to `FormControl`.
154+
*
155+
* @publicApi
156+
*/
157+
export interface FormControlOptions extends AbstractControlOptions {
158+
/**
159+
* @description
160+
* Whether to use the initial value used to construct the FormControl as its default value as
161+
* well. If this option is false or not provided, the default value of a FormControl is `null`.
162+
* When a FormControl is {@link FormControl#reset} without an explicit value, its value reverts to
163+
* its default value.
164+
*/
165+
initialValueIsDefault?: boolean;
166+
}
167+
149168
function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|
150169
null): validatorOrOpts is AbstractControlOptions {
151170
return validatorOrOpts != null && !Array.isArray(validatorOrOpts) &&
@@ -1228,10 +1247,17 @@ export abstract class AbstractControl {
12281247
* @publicApi
12291248
*/
12301249
export class FormControl extends AbstractControl {
1250+
/**
1251+
* The default value of this FormControl, used whenever the control is reset without an explicit
1252+
* value. See {@link FormControlOptions#initialValueIsDefault} for more information on configuring
1253+
* a default value.
1254+
* @publicApi
1255+
*/
1256+
public readonly defaultValue: any = null;
1257+
12311258
/** @internal */
12321259
_onChange: Array<Function> = [];
12331260

1234-
12351261
/**
12361262
* This field holds a pending value that has not yet been applied to the form's value.
12371263
* It is `any` because the value is untyped.
@@ -1256,8 +1282,7 @@ export class FormControl extends AbstractControl {
12561282
*
12571283
*/
12581284
constructor(
1259-
formState: any = null,
1260-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
1285+
formState: any = null, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
12611286
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
12621287
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
12631288
this._applyFormState(formState);
@@ -1271,6 +1296,13 @@ export class FormControl extends AbstractControl {
12711296
// to `true` to allow that during the control creation process.
12721297
emitEvent: !!this.asyncValidator
12731298
});
1299+
if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) {
1300+
if (this._isBoxedValue(formState)) {
1301+
this.defaultValue = formState.value;
1302+
} else {
1303+
this.defaultValue = formState;
1304+
}
1305+
}
12741306
}
12751307

12761308
/**
@@ -1329,8 +1361,23 @@ export class FormControl extends AbstractControl {
13291361
}
13301362

13311363
/**
1332-
* Resets the form control, marking it `pristine` and `untouched`, and setting
1333-
* the value to null.
1364+
* Resets the form control, marking it `pristine` and `untouched`, and resetting
1365+
* the value. The new value will be the provided value (if passed), `null`, or the initial value
1366+
* if `initialValueIsDefault` was set in the constructor via {@link FormControlOptions}.
1367+
*
1368+
* ```ts
1369+
* // By default, the control will reset to null.
1370+
* const dog = new FormControl('spot');
1371+
* dog.reset(); // dog.value is null
1372+
*
1373+
* // If this flag is set, the control will instead reset to the initial value.
1374+
* const cat = new FormControl('tabby', {initialValueIsDefault: true});
1375+
* cat.reset(); // cat.value is "tabby"
1376+
*
1377+
* // A value passed to reset always takes precedence.
1378+
* const fish = new FormControl('finn', {initialValueIsDefault: true});
1379+
* fish.reset('bubble'); // fish.value is "bubble"
1380+
* ```
13341381
*
13351382
* @param formState Resets the control with an initial value,
13361383
* or an object that defines the initial value and disabled state.
@@ -1346,8 +1393,10 @@ export class FormControl extends AbstractControl {
13461393
* When false, no events are emitted.
13471394
*
13481395
*/
1349-
override reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}):
1350-
void {
1396+
override reset(formState: any = this.defaultValue, options: {
1397+
onlySelf?: boolean,
1398+
emitEvent?: boolean
1399+
} = {}): void {
13511400
this._applyFormState(formState);
13521401
this.markAsPristine(options);
13531402
this.markAsUntouched(options);

packages/forms/test/form_control_spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,62 @@ describe('FormControl', () => {
817817
expect(c.value).toBe(null);
818818
});
819819

820+
it('should reset to the initial value if specified in FormControlOptions', () => {
821+
const c2 = new FormControl('foo', {initialValueIsDefault: true});
822+
expect(c2.value).toBe('foo');
823+
expect(c2.defaultValue).toBe('foo');
824+
825+
c2.setValue('bar');
826+
expect(c2.value).toBe('bar');
827+
expect(c2.defaultValue).toBe('foo');
828+
829+
c2.reset();
830+
expect(c2.value).toBe('foo');
831+
expect(c2.defaultValue).toBe('foo');
832+
833+
const c3 = new FormControl('foo', {initialValueIsDefault: false});
834+
expect(c3.value).toBe('foo');
835+
expect(c3.defaultValue).toBe(null);
836+
837+
c3.setValue('bar');
838+
expect(c3.value).toBe('bar');
839+
expect(c3.defaultValue).toBe(null);
840+
841+
c3.reset();
842+
expect(c3.value).toBe(null);
843+
expect(c3.defaultValue).toBe(null);
844+
});
845+
846+
it('should look inside FormState objects for a default value', () => {
847+
const c2 = new FormControl({value: 'foo', disabled: false}, {initialValueIsDefault: true});
848+
expect(c2.value).toBe('foo');
849+
expect(c2.defaultValue).toBe('foo');
850+
851+
c2.setValue('bar');
852+
expect(c2.value).toBe('bar');
853+
expect(c2.defaultValue).toBe('foo');
854+
855+
c2.reset();
856+
expect(c2.value).toBe('foo');
857+
expect(c2.defaultValue).toBe('foo');
858+
});
859+
860+
it('should not alter the disabled state when resetting, even if a default value is provided',
861+
() => {
862+
const c2 = new FormControl({value: 'foo', disabled: true}, {initialValueIsDefault: true});
863+
expect(c2.value).toBe('foo');
864+
expect(c2.defaultValue).toBe('foo');
865+
expect(c2.disabled).toBe(true);
866+
867+
c2.setValue('bar');
868+
c2.enable();
869+
870+
c2.reset();
871+
expect(c2.value).toBe('foo');
872+
expect(c2.defaultValue).toBe('foo');
873+
expect(c2.disabled).toBe(false);
874+
});
875+
820876
it('should update the value of any parent controls with passed value', () => {
821877
const g = new FormGroup({'one': c});
822878
c.setValue('new value');

0 commit comments

Comments
 (0)