diff --git a/goldens/public-api/forms/forms.md b/goldens/public-api/forms/forms.md index 40bfc39c2b1367..cf3fb69377c04b 100644 --- a/goldens/public-api/forms/forms.md +++ b/goldens/public-api/forms/forms.md @@ -21,7 +21,7 @@ import { SimpleChanges } from '@angular/core'; import { Version } from '@angular/core'; // @public -export abstract class AbstractControl { +export abstract class AbstractControl { constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null); addAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void; addValidators(validators: ValidatorFn | ValidatorFn[]): void; @@ -41,7 +41,8 @@ export abstract class AbstractControl { }): void; get enabled(): boolean; readonly errors: ValidationErrors | null; - get(path: Array | string): AbstractControl | null; + get

(path: P): AbstractControl> | null; + get

>(path: P): AbstractControl> | null; getError(errorCode: string, path?: Array | string): any; hasAsyncValidator(validator: AsyncValidatorFn): boolean; hasError(errorCode: string, path?: Array | string): boolean; @@ -64,22 +65,21 @@ export abstract class AbstractControl { markAsUntouched(opts?: { onlySelf?: boolean; }): void; - get parent(): FormGroup | FormArray | null; - abstract patchValue(value: any, options?: Object): void; + get parent(): AbstractControl | null; + abstract patchValue(value: ValueT, options?: Object): void; get pending(): boolean; readonly pristine: boolean; removeAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void; removeValidators(validators: ValidatorFn | ValidatorFn[]): void; - abstract reset(value?: any, options?: Object): void; - get root(): AbstractControl; + abstract reset(value?: RawValueT, options?: Object): void; + get root(): AbstractControl; setAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[] | null): void; setErrors(errors: ValidationErrors | null, opts?: { emitEvent?: boolean; }): void; - // (undocumented) - setParent(parent: FormGroup | FormArray): void; + setParent(parent: AbstractControl | null): void; setValidators(validators: ValidatorFn | ValidatorFn[] | null): void; - abstract setValue(value: any, options?: Object): void; + abstract setValue(value: RawValueT, options?: Object): void; readonly status: FormControlStatus; readonly statusChanges: Observable; readonly touched: boolean; @@ -92,8 +92,8 @@ export abstract class AbstractControl { get valid(): boolean; get validator(): ValidatorFn | null; set validator(validatorFn: ValidatorFn | null); - readonly value: any; - readonly valueChanges: Observable; + readonly value: ValueT; + readonly valueChanges: Observable; } // @public @@ -225,37 +225,37 @@ export interface Form { } // @public -export class FormArray extends AbstractControl { - constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); - at(index: number): AbstractControl; +export class FormArray>, RawValue>>> = any[]> extends AbstractControl, FormArrayRawValue> { + constructor(controls: CtrlT, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + at(index: number): CtrlT[K]; clear(options?: { emitEvent?: boolean; }): void; // (undocumented) - controls: AbstractControl[]; - getRawValue(): any[]; - insert(index: number, control: AbstractControl, options?: { + controls: CtrlT; + getRawValue(): FormArrayRawValue; + insert(index: number, control: ArrayElement, options?: { emitEvent?: boolean; }): void; get length(): number; - patchValue(value: any[], options?: { + patchValue(value: FormArrayValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - push(control: AbstractControl, options?: { + push(control: ArrayElement, options?: { emitEvent?: boolean; }): void; removeAt(index: number, options?: { emitEvent?: boolean; }): void; - reset(value?: any, options?: { + reset(value?: FormArrayValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setControl(index: number, control: AbstractControl, options?: { + setControl(index: number, control: ArrayElement, options?: { emitEvent?: boolean; }): void; - setValue(value: any[], options?: { + setValue(value: FormArrayRawValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; @@ -278,13 +278,22 @@ 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[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl; - group(controlsConfig: { + array(cc: Array | ControlConfig | AbstractControl | FormControl>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>>; + // (undocumented) + control(formState: T | FormState, opts: FormControlOptions & { + initialValueIsDefault: true; + }): FormControl; + // (undocumented) + control(formState: T | FormState, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl; + group(cc: { + [K in keyof T]: FormState | ControlConfig | FormControl | AbstractControl | T[K]; + }, options?: AbstractControlOptions | null): FormGroup<{ + [K in keyof T]: AbstractControl; + }>; // @deprecated - group(controlsConfig: { + group(cc: { [key: string]: any; }, options: { [key: string]: any; @@ -296,9 +305,10 @@ export class FormBuilder { } // @public -export interface FormControl extends AbstractControl { - readonly defaultValue: any; - patchValue(value: any, options?: { +export interface FormControl extends AbstractControl { + readonly defaultValue: ValueT; + getRawValue(): ValueT; + patchValue(value: ValueT, options?: { onlySelf?: boolean; emitEvent?: boolean; emitModelToViewChange?: boolean; @@ -306,11 +316,11 @@ export interface FormControl extends AbstractControl { }): void; registerOnChange(fn: Function): void; registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; - reset(formState?: any, options?: { + reset(formState?: ValueT | FormState, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setValue(value: any, options?: { + setValue(value: ValueT, options?: { onlySelf?: boolean; emitEvent?: boolean; emitModelToViewChange?: boolean; @@ -376,39 +386,35 @@ export interface FormControlOptions extends AbstractControlOptions { export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; // @public -export class FormGroup extends AbstractControl { - constructor(controls: { - [key: string]: AbstractControl; - }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); - addControl(name: string, control: AbstractControl, options?: { +export class FormGroup, RawValue>; +} = { + [key: string]: AbstractControl; +}> extends AbstractControl, FormGroupRawValue> { + constructor(controls: CtrlT, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + addControl(name: K, control: Required[K], options?: { emitEvent?: boolean; }): void; - contains(controlName: string): boolean; + contains(controlName: K): boolean; // (undocumented) - controls: { - [key: string]: AbstractControl; - }; - getRawValue(): any; - patchValue(value: { - [key: string]: any; - }, options?: { + controls: CtrlT; + getRawValue(): FormGroupRawValue; + patchValue(value: FormGroupValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - registerControl(name: string, control: AbstractControl): AbstractControl; - removeControl(name: string, options?: { + registerControl(name: K, control: CtrlT[K]): CtrlT[K]; + removeControl(name: KeyIsRemovable, options?: { emitEvent?: boolean; }): void; - reset(value?: any, options?: { + reset(value?: FormGroupValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setControl(name: string, control: AbstractControl, options?: { + setControl(name: K, control: CtrlT[K], options?: { emitEvent?: boolean; }): void; - setValue(value: { - [key: string]: any; - }, options?: { + setValue(value: FormGroupRawValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index afe8691c8659a4..d699ec7612861e 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -1010,9 +1010,6 @@ { "name": "getPromiseCtor" }, - { - "name": "getRawValue" - }, { "name": "getSelectedIndex" }, @@ -1149,13 +1146,10 @@ "name": "isEmptyInputValue" }, { - "name": "isFormArray" - }, - { - "name": "isFormControl" + "name": "isFormGroup" }, { - "name": "isFormGroup" + "name": "isFormState" }, { "name": "isForwardRef" diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 193bb0cdabaa82..d864df327ea2f4 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -980,9 +980,6 @@ { "name": "getPromiseCtor" }, - { - "name": "getRawValue" - }, { "name": "getSelectedIndex" }, @@ -1121,6 +1118,9 @@ { "name": "isFormGroup" }, + { + "name": "isFormState" + }, { "name": "isForwardRef" }, diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index 0ac96ac290bb9a..0554aee8519a96 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, FormControlOptions, FormGroup, FormHooks, isFormArray, isFormControl, isFormGroup} from './model'; +import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormGroup, FormHooks, FormState} from './model'; function isAbstractControlOptions(options: AbstractControlOptions| {[key: string]: any}): options is AbstractControlOptions { @@ -20,162 +20,195 @@ function isAbstractControlOptions(options: AbstractControlOptions| } /** - * @description - * Creates an `AbstractControl` from a user-specified configuration. - * - * The `FormBuilder` provides syntactic sugar that shortens creating instances of a `FormControl`, - * `FormGroup`, or `FormArray`. It reduces the amount of boilerplate needed to build complex - * forms. - * - * @see [Reactive Forms Guide](/guide/reactive-forms) + * ControlConfig is a tuple containing a value of type T, plus optional validators and async + * validators. * * @publicApi */ -@Injectable({providedIn: ReactiveFormsModule}) -export class FormBuilder { - /** - * @description - * Construct a new `FormGroup` instance. - * - * @param controlsConfig A collection of child controls. The key for each child is the name - * under which it is registered. - * - * @param options Configuration options object for the `FormGroup`. The object should have the - * the `AbstractControlOptions` type and might contain the following fields: - * * `validators`: A synchronous validator function, or an array of validator functions - * * `asyncValidators`: A single async validator or array of async validator functions - * * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur' | - * submit') - */ - group( - controlsConfig: {[key: string]: any}, - options?: AbstractControlOptions|null, - ): FormGroup; - /** - * @description - * Construct a new `FormGroup` instance. - * - * @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming. - * Use the `FormBuilder#group` overload with `AbstractControlOptions` instead. - * Note that `AbstractControlOptions` expects `validators` and `asyncValidators` to be valid - * validators. If you have custom validators, make sure their validation function parameter is - * `AbstractControl` and not a sub-class, such as `FormGroup`. These functions will be called with - * an object of type `AbstractControl` and that cannot be automatically downcast to a subclass, so - * TypeScript sees this as an error. For example, change the `(group: FormGroup) => - * ValidationErrors|null` signature to be `(group: AbstractControl) => ValidationErrors|null`. - * - * @param controlsConfig A collection of child controls. The key for each child is the name - * under which it is registered. - * - * @param options Configuration options object for the `FormGroup`. The legacy configuration - * object consists of: - * * `validator`: A synchronous validator function, or an array of validator functions - * * `asyncValidator`: A single async validator or array of async validator functions - * Note: the legacy format is deprecated and might be removed in one of the next major versions - * of Angular. - */ - group( - controlsConfig: {[key: string]: any}, - options: {[key: string]: any}, - ): FormGroup; - group( - controlsConfig: {[key: string]: any}, - options: AbstractControlOptions|{[key: string]: any}|null = null): FormGroup { - const controls = this._reduceControls(controlsConfig); - - let validators: ValidatorFn|ValidatorFn[]|null = null; - let asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null = null; - let updateOn: FormHooks|undefined = undefined; - - if (options != null) { - if (isAbstractControlOptions(options)) { - // `options` are `AbstractControlOptions` - validators = options.validators != null ? options.validators : null; - asyncValidators = options.asyncValidators != null ? options.asyncValidators : null; - updateOn = options.updateOn != null ? options.updateOn : undefined; - } else { - // `options` are legacy form group options - validators = options['validator'] != null ? options['validator'] : null; - asyncValidators = options['asyncValidator'] != null ? options['asyncValidator'] : null; - } - } - - return new FormGroup(controls, {asyncValidators, updateOn, validators}); - } - - /** - * @description - * Construct a new `FormControl` with the given state, validators and options. - * - * @param formState Initializes the control with an initial state value, or - * with an object that contains both a value and a disabled status. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains - * validation functions and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator - * functions. - * - * @usageNotes - * - * ### Initialize a control as disabled - * - * The following example returns a control with an initial value in a disabled state. - * - * - * - */ - control( - formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl { - return new FormControl(formState, validatorOrOpts, asyncValidator); - } - - /** - * Constructs a new `FormArray` from the given array of configurations, - * validators and options. - * - * @param controlsConfig An array of child controls or control configs. Each - * child control is given an index when it is registered. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains - * validation functions and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator - * functions. - */ - array( - controlsConfig: any[], - validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray { - const controls = controlsConfig.map(c => this._createControl(c)); - return new FormArray(controls, validatorOrOpts, asyncValidator); - } - - /** @internal */ - _reduceControls(controlsConfig: {[k: string]: any}): {[key: string]: AbstractControl} { - const controls: {[key: string]: AbstractControl} = {}; - Object.keys(controlsConfig).forEach(controlName => { - controls[controlName] = this._createControl(controlsConfig[controlName]); - }); - return controls; - } - - /** @internal */ - _createControl(controlConfig: any): AbstractControl { - if (isFormControl(controlConfig) || isFormGroup(controlConfig) || isFormArray(controlConfig)) { - return controlConfig; - - } else if (Array.isArray(controlConfig)) { - const value = controlConfig[0]; - const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null; - const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? controlConfig[2] : null; - return this.control(value, validator, asyncValidator); - - } else { - return this.control(controlConfig); - } - } -} + export type ControlConfig = [T|FormState, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?]; + + /** + * @description + * Creates an `AbstractControl` from a user-specified configuration. + * + * The `FormBuilder` provides syntactic sugar that shortens creating instances of a + * `FormControl`, `FormGroup`, or `FormArray`. It reduces the amount of boilerplate needed to + * build complex forms. + * + * @see [Reactive Forms Guide](/guide/reactive-forms) + * + * @publicApi + */ + @Injectable({providedIn: ReactiveFormsModule}) + export class FormBuilder { + /** + * @description + * Construct a new `FormGroup` instance. + * + * @param cc A collection of child controls. The key for each child is the name + * under which it is registered. + * + * @param options Configuration options object for the `FormGroup`. The object should have the + * the `AbstractControlOptions` type and might contain the following fields: + * * `validators`: A synchronous validator function, or an array of validator functions + * * `asyncValidators`: A single async validator or array of async validator functions + * * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur' | + * submit') + */ + group( + cc: { + [K in keyof T]: FormState|ControlConfig|FormControl| + AbstractControl|T[K] + }, + options?: AbstractControlOptions|null, + ): FormGroup<{[K in keyof T]: AbstractControl}>; + + /** + * @description + * Construct a new `FormGroup` instance. + * + * @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming. + * Use the `FormBuilder#group` overload with `AbstractControlOptions` instead. + * Note that `AbstractControlOptions` expects `validators` and `asyncValidators` to be valid + * validators. If you have custom validators, make sure their validation function parameter is + * `AbstractControl` and not a sub-class, such as `FormGroup`. These functions will be called + * with an object of type `AbstractControl` and that cannot be automatically downcast to a + * subclass, so TypeScript sees this as an error. For example, change the `(group: FormGroup) => + * ValidationErrors|null` signature to be `(group: AbstractControl) => ValidationErrors|null`. + * + * @param cc A collection of child controls. The key for each child is the name + * under which it is registered. + * + * @param options Configuration options object for the `FormGroup`. The legacy configuration + * object consists of: + * * `validator`: A synchronous validator function, or an array of validator functions + * * `asyncValidator`: A single async validator or array of async validator functions + * Note: the legacy format is deprecated and might be removed in one of the next major versions + * of Angular. + */ + group( + cc: {[key: string]: any}, + options: {[key: string]: any}, + ): FormGroup; + + group(cc: {[key: string]: any}, options: AbstractControlOptions|{[key: string]: + any}|null = null): + FormGroup { + const controls = this._reduceControls(cc); + + let validators: ValidatorFn|ValidatorFn[]|null = null; + let asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null = null; + let updateOn: FormHooks|undefined = undefined; + + if (options != null) { + if (isAbstractControlOptions(options)) { + // `options` are `AbstractControlOptions` + validators = options.validators != null ? options.validators : null; + asyncValidators = options.asyncValidators != null ? options.asyncValidators : null; + updateOn = options.updateOn != null ? options.updateOn : undefined; + } else { + // `options` are legacy form group options + validators = options['validator'] != null ? options['validator'] : null; + asyncValidators = options['asyncValidator'] != null ? options['asyncValidator'] : null; + } + } + + return new FormGroup(controls, {asyncValidators, updateOn, validators}); + } + + control(formState: T|FormState, opts: FormControlOptions&{initialValueIsDefault: true}): + FormControl; + + control( + formState: T|FormState, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; + + /** + * @description + * Construct a new `FormControl` with the given state, validators and options. Set + * `{initialValueIsDefault: true}` in the options to get a non-nullable control. otherwise, the + * control will be nullable. + * + * @param formState Initializes the control with an initial state value, or + * with an object that contains both a value and a disabled status. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or an `FormControlOptions` object that contains + * validation functions and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator + * functions. + * + * @usageNotes + * + * ### Initialize a control as disabled + * + * The following example returns a control with an initial value in a disabled state. + * + * + * + */ + control( + formState: T|FormState, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| + null): FormControl|FormControl { + return new FormControl(formState, validatorOrOpts, asyncValidator); + } + + /** + * Constructs a new `FormArray` from the given array of configurations, + * validators and options. + * + * TODO: Do we want to support providing default values to create non-nullable controls? + * + * @param cc An array of child controls or control configs. Each + * child control is given an index when it is registered. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or an `AbstractControlOptions` object that contains + * validation functions and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator + * functions. + */ + array( + cc: Array|ControlConfig|AbstractControl|FormControl>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| + null): FormArray>> { + const controls = cc.map(c => this._createControl(c)); + return new FormArray(controls, validatorOrOpts, asyncValidator); + } + + /** @internal */ + _reduceControls(cc: {[k: string]: T|ControlConfig|FormState|AbstractControl}): + {[key: string]: AbstractControl} { + const controls: {[key: string]: AbstractControl} = {}; + Object.keys(cc).forEach(controlName => { + controls[controlName] = this._createControl(cc[controlName]); + }); + return controls; + } + + /** @internal */ + _createControl(cc: T|FormState|ControlConfig|AbstractControl| + FormControl): AbstractControl|AbstractControl { + if (cc instanceof FormControl) { + return cc as FormControl; + } else if (cc instanceof AbstractControl) { // A control; just return it + return cc; + + } else if (Array.isArray(cc)) { // ControlConfig Array + const value: T|FormState = cc[0]; + const validator: ValidatorFn|ValidatorFn[]|null = cc.length > 1 ? cc[1]! : null; + const asyncValidator: AsyncValidatorFn|AsyncValidatorFn[]|null = + cc.length > 2 ? cc[2]! : null; + return this.control(value, validator, asyncValidator); + + } else { // T or FormState + return this.control(cc); + } + } + } diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index db242a343ce843..a9c8cde09f3d77 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -151,7 +151,7 @@ export interface AbstractControlOptions { } /** - * Interface for options provided to a {@link FormControl}. + * Interface for options provided to a `FormControl`. * * This interface extends all options from {@link AbstractControlOptions}, plus some options * unique to `FormControl`. @@ -161,7 +161,7 @@ export interface AbstractControlOptions { export interface FormControlOptions extends AbstractControlOptions { /** * @description - * Whether to use the initial value used to construct the {@link FormControl} as its default value + * 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 reset without an explicit value, its value reverts to * its default value. @@ -186,7 +186,7 @@ function getRawValue(control: AbstractControl): any { return isFormControl(control) ? control.value : (control as FormGroup | FormArray).getRawValue(); } -function assertControlPresent(parent: FormGroup|FormArray, key: string|number): void { +function assertControlPresent(parent: FormGroup|FormArray, key: string|number): void { const isGroup = isFormGroup(parent); const controls = parent.controls as {[key: string|number]: unknown}; const collection = isGroup ? Object.keys(controls) : controls; @@ -200,7 +200,7 @@ function assertControlPresent(parent: FormGroup|FormArray, key: string|number): } } -function assertAllValuesPresent(control: FormGroup|FormArray, value: any): void { +function assertAllValuesPresent(control: FormGroup|FormArray, value: any): void { const isGroup = isFormGroup(control); control._forEachChild((_: unknown, key: string|number) => { if (value[key] === undefined) { @@ -211,6 +211,87 @@ function assertAllValuesPresent(control: FormGroup|FormArray, value: any): void }); } +/** + * TypedOrUntyped allows one of two different types to be selected, depending on whether the Forms + * Class it's applied to is typed or not. + * + * @publicApi + */ +export type TypedOrUntyped = T extends never ? Untyped : Typed; + +/** + * Value gives the type of `.value` in an AbstractControl. + * + * @publicApi + */ +export type Value = T extends AbstractControl? T['value'] : never; + +/** + * Value gives the type of `.getRawValue()` in an AbstractControl. + * + * @publicApi + */ +export type RawValue = T extends AbstractControl? + (T['setValue'] extends((v: infer R) => void) ? R : never) : + never; + +/** + * Tokenize splits a string literal S by a delimeter D. + */ +type Tokenize = /*\n*/ + string extends S ? string[] : /* S must be a literal */ /*\n*/ + S extends `${infer T}${D}${infer U}` ? [T, ...Tokenize] : /*\n*/ + [S] /* Base case */ /*\n*/ + ; + +/** + * CoerceStrArrToNumArr accepts an array of strings, and converts any numeric string to a number. + */ +type CoerceStrArrToNumArr = /*\n*/ + S extends [infer Head, ...infer Tail] ? /*\n*/ + Head extends `${number}` ? [number, ...CoerceStrArrToNumArr] : /*\n*/ + [Head, ...CoerceStrArrToNumArr] : /*\n*/ + [] /*\n*/ + ; + +/** + * Navigate takes a type T and an array K, and returns the type of T[K[0]][K[1]][K[2]]... + */ +type Navigate)> = /*\n*/ + T extends object ? /* T must be indexable (object or array) */ /*\n*/ + (K extends [infer Head, ...infer Tail] ? /* Split K into head and tail */ /*\n*/ + (Head extends keyof T ? /* head(K) must index T */ /*\n*/ + (Tail extends(string|number)[] ? /* tail(K) must be an array */ /*\n*/ + [] extends Tail ? T[Head] : /* base case: K can be split, but Tail is empty */ /*\n*/ + (Navigate) /* explore T[head(K)] by tail(K) */ : /*\n*/ + any) /* tail(K) was not an array, give up */ : /*\n*/ + any) /* head(K) does not index T, give up */ : /*\n*/ + any) /* K cannot be split, give up */ : /*\n*/ + any /* T is not indexable, give up */ /*\n*/ + ; + +/** + * Writeable removes readonly from all keys. + */ +type Writeable = { + -readonly[P in keyof T]: T[P] +}; + +/** + * GetProperty takes a type T and an some property names or indices K. + * If K is a dot-separated string, it is tokenized into an array before proceeding. + * Then, the type of the nested property at K is computed: T[K[0]][K[1]][K[2]]... + * This works with both objects, which are indexed by property name, and arrays, which are indexed + * numerically. + * + * @publicApi + */ +export type Get = /*\n*/ + K extends string ? Get>>: /*\n*/ + Writeable extends Array? Navigate>: /*\n*/ + any /*\n*/ + ; + /** * This is the base class for `FormControl`, `FormGroup`, and `FormArray`. * @@ -219,13 +300,18 @@ function assertAllValuesPresent(control: FormGroup|FormArray, value: any): void * that are shared between all sub-classes, like `value`, `valid`, and `dirty`. It shouldn't be * instantiated directly. * + * This class has a main type parameter ValueT which represents the value type of the control. + * That type can contain partials, due to disabled controls. The optional type parameter RawValueT + * represents the raw value type (i.e. what the type of `rawValue` would be, or `value` with all + * controls enabled). + * * @see [Forms Guide](/guide/forms) * @see [Reactive Forms Guide](/guide/reactive-forms) * @see [Dynamic Forms Guide](/guide/dynamic-form) * * @publicApi */ -export abstract class AbstractControl { +export abstract class AbstractControl { /** @internal */ _pendingDirty = false; @@ -245,7 +331,7 @@ export abstract class AbstractControl { /** @internal */ _updateOn?: FormHooks; - private _parent: FormGroup|FormArray|null = null; + private _parent: AbstractControl|null = null; private _asyncValidationSubscription: any; /** @@ -296,7 +382,7 @@ export abstract class AbstractControl { * * For a `FormArray`, the values of enabled controls as an array. * */ - public readonly value: any; + public readonly value!: ValueT; /** * Initialize the AbstractControl instance. @@ -342,7 +428,7 @@ export abstract class AbstractControl { /** * The parent control. */ - get parent(): FormGroup|FormArray|null { + get parent(): AbstractControl|null { return this._parent; } @@ -469,7 +555,8 @@ export abstract class AbstractControl { * the UI or programmatically. It also emits an event each time you call enable() or disable() * without passing along {emitEvent: false} as a function argument. */ - public readonly valueChanges!: Observable; + // TODO(issue/24571): remove '!'. + public readonly valueChanges!: Observable; /** * A multicasting observable that emits an event every time the validation `status` of the control @@ -794,7 +881,7 @@ export abstract class AbstractControl { this._updateValue(); if (opts.emitEvent !== false) { - (this.valueChanges as EventEmitter).emit(this.value); + (this.valueChanges as EventEmitter).emit(this.value); (this.statusChanges as EventEmitter).emit(this.status); } @@ -847,26 +934,30 @@ export abstract class AbstractControl { } /** - * @param parent Sets the parent of the control + * Sets the parent of the control + * + * @param parent The new parent. */ - setParent(parent: FormGroup|FormArray): void { + setParent(parent: AbstractControl|null): void { this._parent = parent; } /** * Sets the value of the control. Abstract method (implemented in sub-classes). + * TODO: should we give this the same stronger type guarantee as reset(), or rely on the runtime + * check? */ - abstract setValue(value: any, options?: Object): void; + abstract setValue(value: RawValueT, options?: Object): void; /** * Patches the value of the control. Abstract method (implemented in sub-classes). */ - abstract patchValue(value: any, options?: Object): void; + abstract patchValue(value: ValueT, options?: Object): void; /** * Resets the control. Abstract method (implemented in sub-classes). */ - abstract reset(value?: any, options?: Object): void; + abstract reset(value?: RawValueT, options?: Object): void; /** * Recalculates the value and validation status of the control. @@ -897,7 +988,7 @@ export abstract class AbstractControl { } if (opts.emitEvent !== false) { - (this.valueChanges as EventEmitter).emit(this.value); + (this.valueChanges as EventEmitter).emit(this.value); (this.statusChanges as EventEmitter).emit(this.status); } @@ -970,11 +1061,29 @@ export abstract class AbstractControl { this._updateControlsErrors(opts.emitEvent !== false); } + /** + * Retrieves a child control given the control's name or path. + * + * This signature for get supports strings and `const` arrays (`.get(['foo', 'bar'] as const)`). + */ + get

(path: P): + AbstractControl>|null; + + /** + * Retrieves a child control given the control's name or path. + * + * This signature for `get` supports non-const (mutable) arrays. Inferred type + * information will not be as robust, so prefer to pass a `readonly` array if possible. + */ + get

>(path: P): AbstractControl>|null; + /** * Retrieves a child control given the control's name or path. * * @param path A dot-delimited string or array of string/number values that define the path to the - * control. + * control. If a string is provided, passing it as a string literal will result in improved type + * information. Likewise, if an array is provided, passing it `as const` will cause improved type + * information to be available. * * @usageNotes * ### Retrieve a nested control @@ -985,7 +1094,7 @@ export abstract class AbstractControl { * * -OR- * - * * `this.form.get(['person', 'name']);` + * * `this.form.get(['person', 'name'] as const);` // `as const` gives improved typings * * ### Retrieve a control in a FormArray * @@ -998,7 +1107,7 @@ export abstract class AbstractControl { * * * `this.form.get(['items', 0, 'price']);` */ - get(path: Array|string): AbstractControl|null { + get

(path: P): AbstractControl>|null { return _find(this, path, '.'); } @@ -1071,7 +1180,7 @@ export abstract class AbstractControl { /** * Retrieves the top-level ancestor of this control. */ - get root(): AbstractControl { + get root(): AbstractControl { let x: AbstractControl = this; while (x._parent) { @@ -1096,7 +1205,7 @@ export abstract class AbstractControl { /** @internal */ _initObservables() { - (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); + (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); } @@ -1160,12 +1269,6 @@ export abstract class AbstractControl { /** @internal */ _onDisabledChange: Array<(isDisabled: boolean) => void> = []; - /** @internal */ - _isBoxedValue(formState: any): boolean { - return typeof formState === 'object' && formState !== null && - Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; - } - /** @internal */ _registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; @@ -1188,6 +1291,14 @@ export abstract class AbstractControl { } } +/** + * FormState is a boxed form value. It is an object with a `value` key and a `disabled` key. + */ +export interface FormState { + value: T; + disabled: boolean; +} + /** * Tracks the value and validation status of an individual form control. * @@ -1287,13 +1398,13 @@ export abstract class AbstractControl { * console.log(control.status); // 'DISABLED' * ``` */ -export interface FormControl extends AbstractControl { +export interface 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. */ - readonly defaultValue: any; + readonly defaultValue: ValueT; /** @internal */ _onChange: Function[]; @@ -1303,7 +1414,7 @@ export interface FormControl extends AbstractControl { * It is `any` because the value is untyped. * @internal */ - _pendingValue: any; + _pendingValue: ValueT; /** @internal */ _pendingChange: boolean; @@ -1331,7 +1442,7 @@ export interface FormControl extends AbstractControl { * event to update the model. * */ - setValue(value: any, options?: { + setValue(value: ValueT, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -1347,7 +1458,7 @@ export interface FormControl extends AbstractControl { * * @see `setValue` for options */ - patchValue(value: any, options?: { + patchValue(value: ValueT, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -1387,7 +1498,13 @@ export interface FormControl extends AbstractControl { * When false, no events are emitted. * */ - reset(formState?: any, options?: {onlySelf?: boolean, emitEvent?: boolean}): void; + reset(formState?: ValueT|FormState, options?: {onlySelf?: boolean, emitEvent?: boolean}): + void; + + /** + * For a simple FormControl, the raw value is equivalent to the value. + */ + getRawValue(): ValueT; /** * @internal @@ -1441,7 +1558,7 @@ export interface FormControl extends AbstractControl { _syncPendingControls(): boolean; } -type FormControlInterface = FormControl; +type FormControlInterface = FormControl; /** * Various available constructors for `FormControl`. @@ -1455,7 +1572,7 @@ export interface ɵFormControlCtor { /** * Construct a FormControl with no initial value or validators. */ - new(): FormControl; + new(): FormControl; /** * Creates a new `FormControl` instance. @@ -1467,10 +1584,13 @@ export interface ɵFormControlCtor { * such functions, or a `FormControlOptions` object that contains validation functions * and a validation trigger. * - * @param asyncValidator A single async validator or array of async validator functions. + * @param asyncValidator A single async validator or array of async validator functions */ - new(formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; + new(value: FormState|T, opts: FormControlOptions&{initialValueIsDefault: true}): + FormControl; + new( + value: FormState|T, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; /** * The presence of an explicit `prototype` property provides backwards-compatibility for apps that @@ -1479,22 +1599,29 @@ export interface ɵFormControlCtor { prototype: FormControl; } +function isFormState(formState: unknown): formState is FormState { + return typeof formState === 'object' && formState !== null && + Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; +} + export const FormControl: ɵFormControlCtor = - (class FormControl extends AbstractControl implements FormControlInterface { + (class FormControl extends AbstractControl< + ValueT> implements FormControlInterface { /** @publicApi */ - public readonly defaultValue: any = null; + public readonly defaultValue: ValueT = null as unknown as ValueT; /** @internal */ - _onChange: Function[] = []; + _onChange: Array = []; /** @internal */ - _pendingValue: any; + _pendingValue!: ValueT; /** @internal */ _pendingChange: boolean = false; constructor( - formState: any = null, + // formState and defaultValue will only be null if T is nullable + formState: FormState|ValueT = null as unknown as ValueT, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super( @@ -1511,21 +1638,21 @@ export const FormControl: ɵFormControlCtor = emitEvent: !!this.asyncValidator }); if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) { - if (this._isBoxedValue(formState)) { - (this.defaultValue as any) = formState.value; + if (isFormState(formState)) { + this.defaultValue = formState.value; } else { - (this.defaultValue as any) = formState; + this.defaultValue = formState; } } } - override setValue(value: any, options: { + override setValue(value: ValueT, options: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, emitViewToModelChange?: boolean } = {}): void { - (this as {value: any}).value = this._pendingValue = value; + (this as {value: ValueT}).value = this._pendingValue = value; if (this._onChange.length && options.emitModelToViewChange !== false) { this._onChange.forEach( (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false)); @@ -1533,7 +1660,7 @@ export const FormControl: ɵFormControlCtor = this.updateValueAndValidity(options); } - override patchValue(value: any, options: { + override patchValue(value: ValueT, options: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -1543,15 +1670,19 @@ export const FormControl: ɵFormControlCtor = } override reset( - formState: any = this.defaultValue, + formState: ValueT|FormState = this.defaultValue, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - this._applyFormState(formState); + this._applyFormState(formState as FormState| ValueT); this.markAsPristine(options); this.markAsUntouched(options); this.setValue(this.value, options); this._pendingChange = false; } + getRawValue(): ValueT { + return this.value; + } + /** @internal */ override _updateValue(): void {} @@ -1599,17 +1730,81 @@ export const FormControl: ɵFormControlCtor = return false; } - private _applyFormState(formState: any) { - if (this._isBoxedValue(formState)) { - (this as {value: any}).value = this._pendingValue = formState.value; + private _applyFormState(formState: FormState|ValueT) { + if (isFormState(formState)) { + (this as {value: ValueT}).value = this._pendingValue = formState.value; formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : this.enable({onlySelf: true, emitEvent: false}); } else { - (this as {value: any}).value = this._pendingValue = formState; + (this as {value: ValueT}).value = this._pendingValue = formState; } } }); +/** + * FormGroupValue extracts the type of `.value` from a FormGroup. + * + * @publicApi + */ +export type FormGroupValue = + TypedOrUntyped}>: never, + {[key: string]: AbstractControl}>; + +/** + * FormGroupRawValue extracts the type of `.getRawValue()` from a FormGroup. + * + * @publicApi + */ +export type FormGroupRawValue = + TypedOrUntyped} : never, + {[key: string]: AbstractControl}>; + +/** + * OptionalKeys returns the union of all optional keys in the object. + */ +type OptionalKeys = { + [K in keyof T] -?: undefined extends T[K] ? K : never +}[keyof T]; + +/** + * RequiredKeys returns the union of all required keys in the object. + */ +type RequiredKeys = { + [K in keyof T] -?: undefined extends T[K] ? never : K +}[keyof T]; + +/** + * IndexSignatureOf returns an object having the index signature from T. If no index signature is + * present in T, this will return an empty object. + */ +type IndexSignatureOf = { + [K in keyof T as string extends K ? K : number extends K ? K : never]: T[K] +}; + +/** + * KnownKeys returns all named (non-index) keys in T. + */ +type KnownKeys = { + [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] +}; + +/** + * HasIndexSignature returns T if string has an index signature, else never. + */ +type HasIndexSignature = IndexSignatureOf extends Record? never : T; + +/** + * KeyIsRemovable determines whether V is a removable property in the object T. + * If V is a required named property in T, then V is never removable. + * If V is an optional named property in T, then V is always removable. + * If V is not a named property but an index signature is present in T, then V is removable. + * + * @publicApi + */ +export type KeyIsRemovable = HasIndexSignature extends never ? + OptionalKeys: + V&(V extends RequiredKeys>? never : {}); + /** * Tracks the value and validity state of a group of `FormControl` instances. * @@ -1624,6 +1819,9 @@ export const FormControl: ɵFormControlCtor = * When instantiating a `FormGroup`, pass in a collection of child controls as the first * argument. The key for each child registers the name for the control. * + * `FormGroup` accepts an optional type parameter `CtrlT`, which is an object type with inner + * control types as values. + * * @usageNotes * * ### Create a form group with 2 controls @@ -1680,9 +1878,27 @@ export const FormControl: ɵFormControlCtor = * }, { updateOn: 'blur' }); * ``` * + * ### Using a FormGroup with optional controls + * + * It is possible to have optional controls in a FormGroup. An optional control can be removed later + * using `removeControl`, and can be omitted when calling `reset`. Optional controls must be + * declared optional in the group's type. + * + * ```ts + * const c = new FormGroup<{one?: FormControl}>({ + * one: new FormControl('') + * }); + * ``` + * + * Notice that `c.value.one` has type `string|null|undefined`. This is because calling `c.reset({})` + * without providing the optional key `one` will cause it to become `null`. + * * @publicApi */ -export class FormGroup extends AbstractControl { +export class FormGroup< + CtrlT extends {[K in keyof CtrlT]: AbstractControl, RawValue>} = { + [key: string]: AbstractControl + }> extends AbstractControl, FormGroupRawValue> { /** * Creates a new `FormGroup` instance. * @@ -1697,7 +1913,7 @@ export class FormGroup extends AbstractControl { * */ constructor( - public controls: {[key: string]: AbstractControl}, + public controls: CtrlT, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); @@ -1714,7 +1930,8 @@ export class FormGroup extends AbstractControl { } /** - * Registers a control with the group's list of controls. + * Registers a control with the group's list of controls. In a strongly-typed group, the control + * must be in the group's type (possibly as an optional key). * * This method does not update the value or validity of the control. * Use {@link FormGroup#addControl addControl} instead. @@ -1722,7 +1939,7 @@ export class FormGroup extends AbstractControl { * @param name The control name to register in the collection * @param control Provides the control for the given name */ - registerControl(name: string, control: AbstractControl): AbstractControl { + registerControl(name: K, control: CtrlT[K]): CtrlT[K] { if (this.controls[name]) return this.controls[name]; this.controls[name] = control; control.setParent(this); @@ -1731,7 +1948,8 @@ export class FormGroup extends AbstractControl { } /** - * Add a control to this group. + * Add a control to this group. In a strongly-typed group, the control must be in the group's type + * (possibly as an optional key). * * If a control with a given name already exists, it would *not* be replaced with a new one. * If you want to replace an existing control, use the {@link FormGroup#setControl setControl} @@ -1745,14 +1963,17 @@ export class FormGroup extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * added. When false, no events are emitted. */ - addControl(name: string, control: AbstractControl, options: {emitEvent?: boolean} = {}): void { + addControl(name: K, control: Required[K], options: { + emitEvent?: boolean + } = {}): void { this.registerControl(name, control); this.updateValueAndValidity({emitEvent: options.emitEvent}); this._onCollectionChange(); } /** - * Remove a control from this group. + * Remove a control from this group. In a strongly-typed group, required controls cannot be + * removed. * * This method also updates the value and validity of the control. * @@ -1763,15 +1984,19 @@ export class FormGroup extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * removed. When false, no events are emitted. */ - removeControl(name: string, options: {emitEvent?: boolean} = {}): void { - if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {}); - delete (this.controls[name]); + removeControl(name: KeyIsRemovable, options: { + emitEvent?: boolean + } = {}): void { + if (this.controls[name as keyof CtrlT]) + this.controls[name as keyof CtrlT]._registerOnCollectionChange(() => {}); + delete (this.controls[name as keyof CtrlT]); this.updateValueAndValidity({emitEvent: options.emitEvent}); this._onCollectionChange(); } /** - * Replace an existing control. + * Replace an existing control. In a strongly-typed group, the control must be in the group's type + * (possibly as an optional key). * * If a control with a given name does not exist in this `FormGroup`, it will be added. * @@ -1783,7 +2008,8 @@ export class FormGroup extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * replaced with a new one. When false, no events are emitted. */ - setControl(name: string, control: AbstractControl, options: {emitEvent?: boolean} = {}): void { + setControl(name: K, control: CtrlT[K], options: {emitEvent?: + boolean} = {}): void { if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {}); delete (this.controls[name]); if (control) this.registerControl(name, control); @@ -1801,7 +2027,7 @@ export class FormGroup extends AbstractControl { * * @returns false for disabled controls, true otherwise. */ - contains(controlName: string): boolean { + contains(controlName: K): boolean { return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled; } @@ -1840,12 +2066,15 @@ export class FormGroup extends AbstractControl { * observables emit events with the latest status and value when the control value is updated. * When false, no events are emitted. */ - override setValue( - value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + override setValue(value: FormGroupRawValue, options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { assertAllValuesPresent(this, value); - Object.keys(value).forEach(name => { - assertControlPresent(this, name); - this.controls[name].setValue(value[name], {onlySelf: true, emitEvent: options.emitEvent}); + (Object.keys(value) as Array).forEach(name => { + assertControlPresent(this, name as any); + this.controls[name].setValue( + (value as any)[name], {onlySelf: true, emitEvent: options.emitEvent}); }); this.updateValueAndValidity(options); } @@ -1882,16 +2111,18 @@ export class FormGroup extends AbstractControl { * the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method. */ override patchValue( - value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + value: FormGroupValue, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { // Even though the `value` argument type doesn't allow `null` and `undefined` values, the // `patchValue` can be called recursively and inner data structures might have these values, so // we just ignore such cases when a field containing FormGroup instance receives `null` or // `undefined` as a value. if (value == null /* both `null` and `undefined` */) return; - - Object.keys(value).forEach(name => { + (Object.keys(value) as Array).forEach(name => { if (this.controls[name]) { - this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent: options.emitEvent}); + this.controls[name]!.patchValue( + /* Guaranteed to be present, due to the outer forEach. */ value + [name as keyof FormGroupValue]!, + {onlySelf: true, emitEvent: options.emitEvent}); } }); this.updateValueAndValidity(options); @@ -1899,7 +2130,7 @@ export class FormGroup extends AbstractControl { /** * Resets the `FormGroup`, marks all descendants `pristine` and `untouched` and sets - * the value of all descendants to null. + * the value of all descendants to their default values, or null if no defaults were provided. * * You reset to a specific form state by passing in a map of states * that matches the structure of your form, with control names as keys. The state @@ -1954,33 +2185,34 @@ export class FormGroup extends AbstractControl { * console.log(form.get('first').status); // 'DISABLED' * ``` */ - override reset(value: any = {}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - this._forEachChild((control: AbstractControl, name: string) => { - control.reset(value[name], {onlySelf: true, emitEvent: options.emitEvent}); + override reset(value: FormGroupValue = {} as unknown as FormGroupValue, options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { + this._forEachChild((control, name) => { + control.reset((value as any)[name], {onlySelf: true, emitEvent: options.emitEvent}); }); this._updatePristine(options); this._updateTouched(options); this.updateValueAndValidity(options); } + /** * The aggregate value of the `FormGroup`, including any disabled controls. * * Retrieves all values regardless of disabled status. - * The `value` property is the best way to get the value of the group, because - * it excludes disabled controls in the `FormGroup`. - */ - getRawValue(): any { - return this._reduceChildren( - {}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => { - acc[name] = getRawValue(control); - return acc; - }); + */ + getRawValue(): FormGroupRawValue { + return this._reduceChildren({}, (acc, control, name) => { + (acc as any)[name] = (control as any).getRawValue(); + return acc; + }) as FormGroupRawValue; } /** @internal */ override _syncPendingControls(): boolean { - let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child: AbstractControl) => { + let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child) => { return child._syncPendingControls() ? true : updated; }); if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true}); @@ -1988,19 +2220,19 @@ export class FormGroup extends AbstractControl { } /** @internal */ - override _forEachChild(cb: (v: any, k: string) => void): void { + override _forEachChild(cb: (v: any, k: any) => void): void { Object.keys(this.controls).forEach(key => { // The list of controls can change (for ex. controls might be removed) while the loop // is running (as a result of invoking Forms API in `valueChanges` subscription), so we // have to null check before invoking the callback. - const control = this.controls[key]; + const control = (this.controls as any)[key]; control && cb(control, key); }); } /** @internal */ _setUpControls(): void { - this._forEachChild((control: AbstractControl) => { + this._forEachChild((control) => { control.setParent(this); control._registerOnCollectionChange(this._onCollectionChange); }); @@ -2012,8 +2244,8 @@ export class FormGroup extends AbstractControl { } /** @internal */ - override _anyControls(condition: (c: AbstractControl) => boolean): boolean { - for (const controlName of Object.keys(this.controls)) { + override _anyControls(condition: (control: CtrlT[K]) => boolean): boolean { + for (const controlName of (Object.keys(this.controls) as K[])) { const control = this.controls[controlName]; if (this.contains(controlName) && condition(control)) { return true; @@ -2023,20 +2255,21 @@ export class FormGroup extends AbstractControl { } /** @internal */ - _reduceValue() { - return this._reduceChildren( - {}, (acc: {[k: string]: any}, control: AbstractControl, name: string): any => { - if (control.enabled || this.disabled) { - acc[name] = control.value; - } - return acc; - }); + _reduceValue(): Partial { + let acc: Partial = {}; + return this._reduceChildren(acc, (acc, control, name) => { + if (control.enabled || this.disabled) { + acc[name] = control.value; + } + return acc; + }); } /** @internal */ - _reduceChildren(initValue: T, fn: (acc: T, control: AbstractControl, name: string) => T): T { + _reduceChildren( + initValue: T, fn: (acc: T, control: CtrlT[K], name: K) => T): T { let res = initValue; - this._forEachChild((control: AbstractControl, name: string) => { + this._forEachChild((control: CtrlT[K], name: K) => { res = fn(res, control, name); }); return res; @@ -2044,7 +2277,7 @@ export class FormGroup extends AbstractControl { /** @internal */ override _allControlsDisabled(): boolean { - for (const controlName of Object.keys(this.controls)) { + for (const controlName of (Object.keys(this.controls) as Array)) { if (this.controls[controlName].enabled) { return false; } @@ -2053,6 +2286,37 @@ export class FormGroup extends AbstractControl { } } +/** + * ArrayElement unpacks a provided array to return the inner type, e.g. `string[]` is unpacked to + * `string`. + * + * @publicApi + */ +export type ArrayElement = A extends Array<(infer E)>? E : never; + +/** + * IsArray is used to determine whether a provided type is an array type. + * + * @publicApi + */ +export type IsArray = T extends any[] ? T : never; + +/** + * FormArrayValue extracts the type of `value` from a FormArray. + * + * @publicApi + */ +export type FormArrayValue = + TypedOrUntyped? Array>: never, any[]>; + +/** + * FormArrayRawValue extracts the type of `.getRawValue()` from a FormArray. + * + * @publicApi + */ +export type FormArrayRawValue = + TypedOrUntyped? Array>: never, any[]>; + /** * Tracks the value and validity state of an array of `FormControl`, * `FormGroup` or `FormArray` instances. @@ -2064,6 +2328,10 @@ export class FormGroup extends AbstractControl { * `FormArray` is one of the three fundamental building blocks used to define forms in Angular, * along with `FormControl` and `FormGroup`. * + * `FormArray` accepts an optional type parameter `CtrlT`, which should be an array containing the + * types of inner controls. If an untyped `FormArray` is desired, `any[]` should be explicitly + * specified. + * * @usageNotes * * ### Create an array of form controls @@ -2117,7 +2385,10 @@ export class FormGroup extends AbstractControl { * * @publicApi */ -export class FormArray extends AbstractControl { +export class FormArray< + CtrlT extends + Array>, RawValue>>> = + any[]> extends AbstractControl, FormArrayRawValue> { /** * Creates a new `FormArray` instance. * @@ -2132,7 +2403,7 @@ export class FormArray extends AbstractControl { * */ constructor( - public controls: AbstractControl[], + public controls: CtrlT, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); @@ -2143,8 +2414,8 @@ export class FormArray extends AbstractControl { onlySelf: true, // If `asyncValidator` is present, it will trigger control status change from `PENDING` to // `VALID` or `INVALID`. - // The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent` - // to `true` to allow that during the control creation process. + // The status should be broadcasted via the `statusChanges` observable, so we set + // `emitEvent` to `true` to allow that during the control creation process. emitEvent: !!this.asyncValidator }); } @@ -2156,8 +2427,8 @@ export class FormArray extends AbstractControl { * around from the back, and if index is greatly negative (less than `-length`), the result is * undefined. This behavior is the same as `Array.at(index)`. */ - at(index: number): AbstractControl { - return this.controls[this._adjustIndex(index)]; + at(index: K): CtrlT[K] { + return this.controls[this._adjustIndex(index as number) as K]; } /** @@ -2170,7 +2441,7 @@ export class FormArray extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * inserted. When false, no events are emitted. */ - push(control: AbstractControl, options: {emitEvent?: boolean} = {}): void { + push(control: ArrayElement, options: {emitEvent?: boolean} = {}): void { this.controls.push(control); this._registerControl(control); this.updateValueAndValidity({emitEvent: options.emitEvent}); @@ -2190,7 +2461,7 @@ export class FormArray extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * inserted. When false, no events are emitted. */ - insert(index: number, control: AbstractControl, options: {emitEvent?: boolean} = {}): void { + insert(index: number, control: ArrayElement, options: {emitEvent?: boolean} = {}): void { this.controls.splice(index, 0, control); this._registerControl(control); @@ -2233,7 +2504,8 @@ export class FormArray extends AbstractControl { * `valueChanges` observables emit events with the latest status and value when the control is * replaced with a new one. When false, no events are emitted. */ - setControl(index: number, control: AbstractControl, options: {emitEvent?: boolean} = {}): void { + setControl(index: number, control: ArrayElement, options: {emitEvent?: boolean} = {}): + void { // Adjust the index, then clamp it at no less than 0 to prevent undesired underflows. let adjustedIndex = this._adjustIndex(index); if (adjustedIndex < 0) adjustedIndex = 0; @@ -2293,11 +2565,14 @@ export class FormArray extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - override setValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + override setValue(value: FormArrayRawValue, options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { assertAllValuesPresent(this, value); value.forEach((newValue: any, index: number) => { assertControlPresent(this, index); - this.at(index).setValue(newValue, {onlySelf: true, emitEvent: options.emitEvent}); + (this.at(index) as any).setValue(newValue, {onlySelf: true, emitEvent: options.emitEvent}); }); this.updateValueAndValidity(options); } @@ -2330,20 +2605,22 @@ export class FormArray extends AbstractControl { * * `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. The configuration options are passed to + * `valueChanges` observables emit events with the latest status and value when the control + * value is updated. When false, no events are emitted. The configuration options are passed to * the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method. */ - override patchValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + override patchValue( + value: FormArrayValue, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { // Even though the `value` argument type doesn't allow `null` and `undefined` values, the - // `patchValue` can be called recursively and inner data structures might have these values, so - // we just ignore such cases when a field containing FormArray instance receives `null` or + // `patchValue` can be called recursively and inner data structures might have these values, + // so we just ignore such cases when a field containing FormArray instance receives `null` or // `undefined` as a value. if (value == null /* both `null` and `undefined` */) return; - value.forEach((newValue: any, index: number) => { + value.forEach((newValue, index) => { if (this.at(index)) { - this.at(index).patchValue(newValue, {onlySelf: true, emitEvent: options.emitEvent}); + (this.at(index) as any) + .patchValue(newValue, {onlySelf: true, emitEvent: options.emitEvent}); } }); this.updateValueAndValidity(options); @@ -2395,7 +2672,10 @@ export class FormArray extends AbstractControl { * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity * updateValueAndValidity} method. */ - override reset(value: any = [], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + override reset(value: FormArrayValue = [] as unknown as FormArrayValue, options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { this._forEachChild((control: AbstractControl, index: number) => { control.reset(value[index], {onlySelf: true, emitEvent: options.emitEvent}); }); @@ -2408,10 +2688,11 @@ export class FormArray extends AbstractControl { * The aggregate value of the array, including any disabled controls. * * Reports all values regardless of disabled status. - * For enabled controls only, the `value` property is the best way to get the value of the array. */ - getRawValue(): any[] { - return this.controls.map((control: AbstractControl) => getRawValue(control)); + getRawValue(): FormArrayRawValue { + return this.controls.map((control, idx) => { + return control instanceof FormControl ? control.value : (control as any).getRawValue(); + }) as FormArrayRawValue; } /** @@ -2452,14 +2733,14 @@ export class FormArray extends AbstractControl { */ clear(options: {emitEvent?: boolean} = {}): void { if (this.controls.length < 1) return; - this._forEachChild((control: AbstractControl) => control._registerOnCollectionChange(() => {})); + this._forEachChild((control) => control._registerOnCollectionChange(() => {})); this.controls.splice(0); this.updateValueAndValidity({emitEvent: options.emitEvent}); } /** - * Adjusts a negative index by summing it with the length of the array. For very negative indices, - * the result may remain negative. + * Adjusts a negative index by summing it with the length of the array. For very negative + * indices, the result may remain negative. * @internal */ private _adjustIndex(index: number): number { @@ -2468,7 +2749,7 @@ export class FormArray extends AbstractControl { /** @internal */ override _syncPendingControls(): boolean { - let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => { + let subtreeUpdated = this.controls.reduce((updated, child) => { return child._syncPendingControls() ? true : updated; }, false); if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true}); @@ -2484,19 +2765,18 @@ export class FormArray extends AbstractControl { /** @internal */ override _updateValue(): void { - (this as {value: any}).value = - this.controls.filter((control) => control.enabled || this.disabled) - .map((control) => control.value); + (this as any).value = this.controls.filter((control) => control.enabled || this.disabled) + .map((control) => control.value); } /** @internal */ override _anyControls(condition: (c: AbstractControl) => boolean): boolean { - return this.controls.some((control: AbstractControl) => control.enabled && condition(control)); + return this.controls.some((control) => control.enabled && condition(control)); } /** @internal */ _setUpControls(): void { - this._forEachChild((control: AbstractControl) => this._registerControl(control)); + this._forEachChild((control) => this._registerControl(control)); } /** @internal */ diff --git a/packages/forms/test/typed_integration_spec.ts b/packages/forms/test/typed_integration_spec.ts new file mode 100644 index 00000000000000..cef7ccf1cfc6da --- /dev/null +++ b/packages/forms/test/typed_integration_spec.ts @@ -0,0 +1,760 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// These tests check only the types of strongly typed form controls. They do not validate behavior, +// except where the behavior shuold differ according to the provided types. +// For behavior tests, see the other specs in this directory. + +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +import {FormArray, FormControl, FormGroup} from '../src/forms'; + +(function() { +describe('FormControl', () => { + it('should support inferred controls', () => { + const c = new FormControl('', {initialValueIsDefault: true}); + { + type ValueType = string; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = string; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.setValue(''); + c.patchValue(''); + c.reset(''); + }); + + it('should support explicit controls', () => { + const c = new FormControl('', {initialValueIsDefault: true}); + { + type ValueType = string; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = string; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.setValue(''); + c.patchValue(''); + c.reset(''); + }); + + it('should support nullable controls', () => { + const c = new FormControl(''); + { + type ValueType = string|null; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = string|null; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.setValue(null); + c.setValue(''); + c.patchValue(null); + c.patchValue(''); + c.reset(); + c.reset(''); + }); + + it('should implicitly create a nullable control when no default is provided', () => { + const c = new FormControl(''); + { + type ValueType = string|null; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = string|null; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.setValue(null); + c.setValue(''); + c.patchValue(null); + c.patchValue(''); + c.reset(); + c.reset(''); + }); +}); + +describe('FormGroup', () => { + it('should support inferred groups', () => { + const c = new FormGroup({ + c: new FormControl('', {initialValueIsDefault: true}), + d: new FormControl(0, {initialValueIsDefault: true}) + }); + { + type ValueType = Partial<{c: string, d: number}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {c: string, d: number}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('c', new FormControl('', {initialValueIsDefault: true})); + c.addControl('c', new FormControl('', {initialValueIsDefault: true})); + c.setControl('c', new FormControl('', {initialValueIsDefault: true})); + c.contains('c'); + c.setValue({c: '', d: 0}); + c.patchValue({c: ''}); + c.reset({c: '', d: 0}); + }); + + it('should support explicit groups', () => { + const c = new FormGroup<{c: FormControl, d: FormControl}>({ + c: new FormControl('', {initialValueIsDefault: true}), + d: new FormControl(0, {initialValueIsDefault: true}) + }); + { + type ValueType = Partial<{c: string, d: number}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {c: string, d: number}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('c', new FormControl('', {initialValueIsDefault: true})); + c.addControl('c', new FormControl('', {initialValueIsDefault: true})); + c.setControl('c', new FormControl('', {initialValueIsDefault: true})); + c.contains('c'); + c.setValue({c: '', d: 0}); + c.patchValue({c: ''}); + c.reset({c: '', d: 0}); + }); + + it('should support groups with nullable controls', () => { + const c = new FormGroup({ + c: new FormControl(''), + d: new FormControl('', {initialValueIsDefault: true}) + }); + { + type ValueType = Partial<{c: string | null, d: string}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {c: string | null, d: string}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('c', new FormControl(null)); + c.addControl('c', new FormControl(null)); + c.setControl('c', new FormControl(null)); + c.contains('c'); + c.setValue({c: '', d: ''}); + c.setValue({c: null, d: ''}); + c.patchValue({}); + c.reset({}); + c.reset({d: ''}); + c.reset({c: ''}); + c.reset({c: '', d: ''}); + }); + + it('should support untyped groups', () => { + const c = new FormGroup({ + c: new FormControl('', {initialValueIsDefault: true}), + d: new FormControl('', {initialValueIsDefault: true}) + }); + c.value; + c.reset(); + c.reset({c: ''}); + c.reset({c: '', d: ''}); + c.reset({c: '', d: ''}, {}); + c.setValue({c: '', d: ''}); + c.setControl('c', new FormControl(0)); + }); + + it('should support groups with explicit named interface types', () => { + interface cat { + lives: number; + } + interface catControls { + lives: FormControl; + } + const g = {lives: new FormControl(9, {initialValueIsDefault: true})}; + const c = new FormGroup(g); + { + type ValueType = Partial<{lives: number}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {lives: number}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('lives', new FormControl(0, {initialValueIsDefault: true})); + c.addControl('lives', new FormControl(0, {initialValueIsDefault: true})); + c.setControl('lives', new FormControl(0, {initialValueIsDefault: true})); + c.contains('lives'); + c.setValue({lives: 0}); + c.patchValue({}); + c.reset({lives: 0}); + }); + + it('should support groups with nested explicit named interface types', () => { + interface cat { + lives: number; + } + interface catControls { + lives: FormControl; + } + const g = {lives: new FormControl(9, {initialValueIsDefault: true})}; + const c = new FormGroup<{a: FormGroup}>({a: new FormGroup(g)}); + }); + + it('should support groups with union types', () => { + interface cat { + lives: number; + } + interface person { + nickname: string; + } + interface catControls { + lives: FormControl; + } + interface personControls { + nickname: FormControl; + } + const kitty = new FormGroup({lives: new FormControl(9, {initialValueIsDefault: true})}); + const billy = + new FormGroup({nickname: new FormControl('billy', {initialValueIsDefault: true})}); + const c = new FormGroup<{who: FormGroup}>({who: kitty}); + { + type ValueType = Partial<{who: Partial}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {who: cat | person}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + const kittyValue = {lives: 9}; + const billyValue = {nickname: 'billy'}; + c.registerControl('who', kitty); + c.registerControl('who', billy); + c.addControl('who', kitty); + c.addControl('who', billy); + c.setControl('who', kitty); + c.setValue({who: kittyValue}); + c.setControl('who', billy); + c.setValue({who: billyValue}); + c.contains('who'); + c.patchValue({}); + c.patchValue({who: kittyValue}); + c.patchValue({who: billyValue}); + c.reset({who: kittyValue}); + c.reset({who: billyValue}); + }); + + it('should support nested inferred groups', () => { + const c = new FormGroup({ + innerGroup: new FormGroup({innerControl: new FormControl('', {initialValueIsDefault: true})}) + }); + { + type ValueType = Partial<{innerGroup: Partial<{innerControl: string}>}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {innerGroup: {innerControl: string}}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl( + 'innerGroup', + new FormGroup({innerControl: new FormControl('', {initialValueIsDefault: true})})); + c.addControl( + 'innerGroup', + new FormGroup({innerControl: new FormControl('', {initialValueIsDefault: true})})); + c.setControl( + 'innerGroup', + new FormGroup({innerControl: new FormControl('', {initialValueIsDefault: true})})); + c.contains('innerGroup'); + c.setValue({innerGroup: {innerControl: ''}}); + c.patchValue({}); + c.reset({innerGroup: {innerControl: ''}}); + }); + + it('should support nested explicit groups', () => { + const ig = new FormControl('', {initialValueIsDefault: true}); + const og = new FormGroup({innerControl: ig}); + const c = new FormGroup<{innerGroup: FormGroup<{innerControl: FormControl}>}>( + {innerGroup: og}); + { + type ValueType = Partial<{innerGroup: Partial<{innerControl: string}>}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {innerGroup: {innerControl: string}}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + // Methods are tested in the inferred case + }); + + it('should support groups with a single optional control', () => { + const c = new FormGroup<{c?: FormControl}>({ + c: new FormControl('', {initialValueIsDefault: true}), + }); + { + type ValueType = Partial<{c?: string}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {c?: string}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('should support groups with mixed optional controls', () => { + const c = new FormGroup<{c?: FormControl, d: FormControl}>({ + c: new FormControl('', {initialValueIsDefault: true}), + d: new FormControl('', {initialValueIsDefault: true}) + }); + { + type ValueType = Partial<{c?: string, d: string}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {c?: string, d: string}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('c', new FormControl('', {initialValueIsDefault: true})); + c.addControl('c', new FormControl('', {initialValueIsDefault: true})); + c.removeControl('c'); + // c.removeControl('d'); // This is not allowed + c.setControl('c', new FormControl('', {initialValueIsDefault: true})); + c.contains('c'); + // c.setValue({d: ''}); // This is not allowed + c.setValue({c: '', d: ''}); + c.patchValue({}); + c.reset({}); + c.reset({c: ''}); + c.reset({d: ''}); + c.reset({c: '', d: ''}); + }); + + it('should support groups with inferred nested arrays', () => { + const arr = new FormArray([new FormControl('', {initialValueIsDefault: true})]); + const c = new FormGroup({a: arr}); + { + type ValueType = Partial<{a: Array}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {a: Array}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + c.registerControl('a', new FormArray([ + new FormControl('', {initialValueIsDefault: true}), + new FormControl('', {initialValueIsDefault: true}) + ])); + c.registerControl('a', new FormArray([new FormControl('', {initialValueIsDefault: true})])); + c.registerControl('a', new FormArray([])); + c.addControl('a', new FormArray([ + new FormControl('', {initialValueIsDefault: true}), + new FormControl('', {initialValueIsDefault: true}) + ])); + c.addControl('a', new FormArray([new FormControl('', {initialValueIsDefault: true})])); + c.addControl('a', new FormArray([])); + c.setControl('a', new FormArray([ + new FormControl('', {initialValueIsDefault: true}), + new FormControl('', {initialValueIsDefault: true}) + ])); + c.setControl('a', new FormArray([new FormControl('', {initialValueIsDefault: true})])); + c.setControl('a', new FormArray([])); + c.contains('a'); + c.patchValue({a: ['', '']}); + c.patchValue({a: ['']}); + c.patchValue({a: []}); + c.patchValue({}); + c.reset({a: ['', '']}); + c.reset({a: ['']}); + c.reset({a: []}); + }); + + it('should support groups with explicit nested arrays', () => { + const arr = new FormArray>>( + [new FormControl('', {initialValueIsDefault: true})]); + const c = new FormGroup<{a: FormArray>>}>({a: arr}); + { + type ValueType = Partial<{a: Array}>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {a: Array}; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + // Methods are tested in the inferred case + }); + + it('should support a complex, deeply nested case', () => { + const v = { + venue: new FormGroup({ + location: new FormControl('San Francisco', {initialValueIsDefault: true}), + date: new FormGroup({ + year: new FormControl(2022, {initialValueIsDefault: true}), + month: new FormControl('May', {initialValueIsDefault: true}), + day: new FormControl(1, {initialValueIsDefault: true}), + }), + }), + dinnerOptions: new FormArray([ + new FormGroup({ + food: new FormGroup({ + entree: new FormControl('Baked Tofu', {initialValueIsDefault: true}), + dessert: new FormControl('Cheesecake', {initialValueIsDefault: true}), + }), + price: new FormGroup({ + amount: new FormControl(10, {initialValueIsDefault: true}), + currency: new FormControl('USD', {initialValueIsDefault: true}), + }), + }), + new FormGroup({ + food: new FormGroup({ + entree: new FormControl('Eggplant Parm', {initialValueIsDefault: true}), + dessert: new FormControl('Chocolate Mousse', {initialValueIsDefault: true}), + }), + price: new FormGroup({ + amount: new FormControl(12, {initialValueIsDefault: true}), + currency: new FormControl('USD', {initialValueIsDefault: true}), + }), + }) + ]) + }; + const c = new FormGroup(v); + { + type ValueType = Partial<{ + venue: Partial<{ + location: string, + date: Partial<{ + year: number, + month: string, + day: number, + }>, + }>, + dinnerOptions: Partial<{ + food: Partial<{ + entree: string, + dessert: string, + }>, + price: Partial<{ + amount: number, + currency: string, + }>, + }>[], + }>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = { + venue: { + location: string, + date: { + year: number, + month: string, + day: number, + }, + }, + dinnerOptions: { + food: { + entree: string, + dessert: string, + }, + price: { + amount: number, + currency: string, + }, + }[], + }; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('should support groups with an index type', () => { + interface AddressBookValues { + returnIfFound: string; + [name: string]: string; + } + interface AddressBookControls { + returnIfFound: FormControl; + [name: string]: FormControl; + } + const c = new FormGroup({ + returnIfFound: new FormControl('1234 Geary, San Francisco', {initialValueIsDefault: true}), + alex: new FormControl('999 Valencia, San Francisco', {initialValueIsDefault: true}), + andrew: new FormControl('100 Lombard, San Francisco', {initialValueIsDefault: true}) + }); + { + type ValueType = Partial; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = AddressBookValues; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + // Named fields. + c.registerControl( + 'returnIfFound', + new FormControl('200 Ellis, San Francisco', {initialValueIsDefault: true})); + c.addControl( + 'returnIfFound', + new FormControl('200 Ellis, San Francisco', {initialValueIsDefault: true})); + c.setControl( + 'returnIfFound', + new FormControl('200 Ellis, San Francisco', {initialValueIsDefault: true})); + // c.removeControl('returnIfFound'); // Not allowed + c.contains('returnIfFound'); + c.setValue({returnIfFound: '200 Ellis, San Francisco', alex: '1 Main', andrew: '2 Main'}); + c.patchValue({}); + c.reset({returnIfFound: '200 Ellis, San Francisco'}); + // Indexed fields. + c.registerControl( + 'igor', new FormControl('300 Page, San Francisco', {initialValueIsDefault: true})); + c.addControl('igor', new FormControl('300 Page, San Francisco', {initialValueIsDefault: true})); + c.removeControl('igor'); + c.setControl('igor', new FormControl('300 Page, San Francisco', {initialValueIsDefault: true})); + c.contains('igor'); + c.setValue({ + returnIfFound: '200 Ellis, San Francisco', + igor: '300 Page, San Francisco', + alex: '1 Main', + andrew: '2 Page', + }); + c.patchValue({}); + c.reset({returnIfFound: '200 Ellis, San Francisco', igor: '300 Page, San Francisco'}); + }); + + it('should have strongly-typed get', () => { + const c = new FormGroup({ + venue: new FormGroup({ + address: new FormControl('2200 Bryant', {initialValueIsDefault: true}), + date: new FormGroup({ + day: new FormControl(21, {initialValueIsDefault: true}), + month: new FormControl('March', {initialValueIsDefault: true}) + }) + }) + }); + const rv = c.getRawValue(); + { + type ValueType = {day: number, month: string}; + let t: ValueType = c.get('venue.date')!.value; + let t1 = c.get('venue.date')!.value; + t1 = null as unknown as ValueType; + } + { + type ValueType = string; + let t: ValueType = c.get('venue.date.month')!.value; + let t1 = c.get('venue.date.month')!.value; + t1 = null as unknown as ValueType; + } + { + type ValueType = string; + let t: ValueType = c.get(['venue', 'date', 'month'] as const)!.value; + let t1 = c.get(['venue', 'date', 'month'] as const)!.value; + t1 = null as unknown as ValueType; + } + }); +}); + +describe('FormArray', () => { + it('should support inferred arrays', () => { + const c = new FormArray([new FormControl('', {initialValueIsDefault: true})]); + { + type ValueType = string[]; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + c.at(0); + c.push(new FormControl('', {initialValueIsDefault: true})); + c.insert(0, new FormControl('', {initialValueIsDefault: true})); + c.removeAt(0); + c.setControl(0, new FormControl('', {initialValueIsDefault: true})); + c.setValue(['', '']); + c.patchValue([]); + c.patchValue(['']); + c.reset(); + c.reset([]); + c.reset(['']); + c.clear(); + }); + + it('should support explicit arrays', () => { + const c = new FormArray>>( + [new FormControl('', {initialValueIsDefault: true})]); + { + type ValueType = string[]; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + }); + + it('should support untyped arrays', () => { + const c = new FormArray([new FormControl('', {initialValueIsDefault: true})]); + { + type ValueType = any[]; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + c.at(0); + c.push(new FormControl('', {initialValueIsDefault: true})); + c.insert(0, new FormControl('', {initialValueIsDefault: true})); + c.removeAt(0); + c.setControl(0, new FormControl('', {initialValueIsDefault: true})); + c.setValue(['', '']); + c.patchValue([]); + c.patchValue(['']); + c.reset(); + c.reset(['']); + c.clear(); + }); + + it('should support arrays with nullable controls', () => { + const c = new FormArray([new FormControl('')]); + { + type ValueType = Array; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + c.at(0); + c.push(new FormControl(null)); + c.insert(0, new FormControl(null)); + c.removeAt(0); + c.setControl(0, new FormControl(null)); + c.setValue(['', '']); + c.patchValue([]); + c.patchValue(['']); + c.reset(); + c.reset([]); + c.reset(['']); + c.clear(); + }); + + it('should support inferred nested arrays', () => { + const c = new FormArray([new FormArray([new FormControl('', {initialValueIsDefault: true})])]); + { + type ValueType = Array>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + }); + + it('should support explicit nested arrays', () => { + const c = new FormArray>>>>( + [new FormArray([new FormControl('', {initialValueIsDefault: true})])]); + { + type ValueType = Array>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + }); + + it('should support arrays with inferred nested groups', () => { + const fg = new FormGroup({c: new FormControl('', {initialValueIsDefault: true})}); + const c = new FormArray([fg]); + c.controls; + { + type ValueType = Array>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = Array<{c: string}>; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('should support arrays with explicit nested groups', () => { + const fg = new FormGroup<{c: FormControl}>( + {c: new FormControl('', {initialValueIsDefault: true})}); + const c = new FormArray}>>>([fg]); + { + type ValueType = Array>; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = Array<{c: string}>; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); +}); +})();