diff --git a/aio/content/examples/ngmodules/src/app/contact/contact.component.ts b/aio/content/examples/ngmodules/src/app/contact/contact.component.ts index 66977633543ae..f87777ceeb15b 100644 --- a/aio/content/examples/ngmodules/src/app/contact/contact.component.ts +++ b/aio/content/examples/ngmodules/src/app/contact/contact.component.ts @@ -1,14 +1,15 @@ // Exact copy except import UserService from greeting -import { Component, OnInit } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; +import {Component, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; -import { Contact, ContactService } from './contact.service'; -import { UserService } from '../greeting/user.service'; +import {UserService} from '../greeting/user.service'; + +import {Contact, ContactService} from './contact.service'; @Component({ selector: 'app-contact', templateUrl: './contact.component.html', - styleUrls: [ './contact.component.css' ] + styleUrls: ['./contact.component.css'] }) export class ContactComponent implements OnInit { contact!: Contact; @@ -17,11 +18,11 @@ export class ContactComponent implements OnInit { msg = 'Loading contacts ...'; userName = ''; - contactForm = this.fb.group({ - name: ['', Validators.required] - }); + contactForm: FormGroup; - constructor(private contactService: ContactService, userService: UserService, private fb: FormBuilder) { + constructor( + private contactService: ContactService, userService: UserService, private fb: FormBuilder) { + this.contactForm = this.fb.group({name: ['', Validators.required]}); this.userName = userService.userName; } @@ -40,7 +41,9 @@ export class ContactComponent implements OnInit { next() { let ix = 1 + this.contacts.indexOf(this.contact); - if (ix >= this.contacts.length) { ix = 0; } + if (ix >= this.contacts.length) { + ix = 0; + } this.contact = this.contacts[ix]; console.log(this.contacts[ix]); } diff --git a/goldens/public-api/forms/index.md b/goldens/public-api/forms/index.md index 9ca809878cb88..522cc39b04b8a 100644 --- a/goldens/public-api/forms/index.md +++ b/goldens/public-api/forms/index.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<ɵGetProperty> | null; + get

>(path: P): AbstractControl<ɵGetProperty> | null; getError(errorCode: string, path?: Array | string): any; getRawValue(): any; hasAsyncValidator(validator: AsyncValidatorFn): boolean; @@ -66,21 +67,20 @@ export abstract class AbstractControl { onlySelf?: boolean; }): void; get parent(): FormGroup | FormArray | null; - abstract patchValue(value: any, options?: Object): void; + abstract patchValue(value: TValue, 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; + abstract reset(value?: TValue, 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: FormGroup | FormArray | null): void; setValidators(validators: ValidatorFn | ValidatorFn[] | null): void; - abstract setValue(value: any, options?: Object): void; + abstract setValue(value: TRawValue, options?: Object): void; readonly status: FormControlStatus; readonly statusChanges: Observable; readonly touched: boolean; @@ -93,8 +93,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: TValue; + readonly valueChanges: Observable; } // @public @@ -223,37 +223,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 = any> extends AbstractControl<ɵTypedOrUntyped, any>, ɵTypedOrUntyped, any>> { + constructor(controls: Array, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + at(index: number): ɵTypedOrUntyped>; clear(options?: { emitEvent?: boolean; }): void; // (undocumented) - controls: AbstractControl[]; - getRawValue(): any[]; - insert(index: number, control: AbstractControl, options?: { + controls: ɵTypedOrUntyped, Array>>; + getRawValue(): ɵFormArrayRawValue; + insert(index: number, control: TControl, 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: TControl, options?: { emitEvent?: boolean; }): void; removeAt(index: number, options?: { emitEvent?: boolean; }): void; - reset(value?: any, options?: { + reset(value?: ɵTypedOrUntyped, any>, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setControl(index: number, control: AbstractControl, options?: { + setControl(index: number, control: TControl, options?: { emitEvent?: boolean; }): void; - setValue(value: any[], options?: { + setValue(value: ɵFormArrayRawValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; @@ -276,13 +276,32 @@ 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: { - [key: string]: any; - }, options?: AbstractControlOptions | null): FormGroup; + // (undocumented) + array(controls: Array>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + // (undocumented) + array; + }>(controls: Array>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + // (undocumented) + array>(controls: Array>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + // (undocumented) + array>(controls: Array>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + // (undocumented) + array(controls: Array | ControlConfig | T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray>; + // (undocumented) + control(formState: T | FormControlState, opts: FormControlOptions & { + initialValueIsDefault: true; + }): FormControl; + // (undocumented) + control(formState: T | FormControlState, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl; + // (undocumented) + group | ControlConfig | FormControl | FormGroup | FormArray | AbstractControl | T[K]; + }>(controls: T, options?: AbstractControlOptions | null): FormGroup<{ + [K in keyof T]: ɵGroupElement; + }>; // @deprecated - group(controlsConfig: { + group(controls: { [key: string]: any; }, options: { [key: string]: any; @@ -294,9 +313,10 @@ export class FormBuilder { } // @public -export interface FormControl extends AbstractControl { - readonly defaultValue: any; - patchValue(value: any, options?: { +export interface FormControl extends AbstractControl { + readonly defaultValue: TValue; + getRawValue(): TValue; + patchValue(value: TValue, options?: { onlySelf?: boolean; emitEvent?: boolean; emitModelToViewChange?: boolean; @@ -304,11 +324,11 @@ export interface FormControl extends AbstractControl { }): void; registerOnChange(fn: Function): void; registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; - reset(formState?: any, options?: { + reset(formState?: TValue | FormControlState, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setValue(value: any, options?: { + setValue(value: TValue, options?: { onlySelf?: boolean; emitEvent?: boolean; emitModelToViewChange?: boolean; @@ -370,43 +390,74 @@ export interface FormControlOptions extends AbstractControlOptions { initialValueIsDefault?: boolean; } +// @public +export interface FormControlState { + // (undocumented) + disabled: boolean; + // (undocumented) + value: T; +} + // @public 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; +} = any> extends AbstractControl<ɵTypedOrUntyped, any>, ɵTypedOrUntyped, any>> { + constructor(controls: TControl, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + addControl(this: FormGroup<{ + [key: string]: AbstractControl; + }>, name: string, control: AbstractControl, options?: { emitEvent?: boolean; }): void; - contains(controlName: string): boolean; // (undocumented) - controls: { - [key: string]: AbstractControl; - }; - getRawValue(): any; - patchValue(value: { - [key: string]: any; - }, options?: { + addControl(name: K, control: Required[K], options?: { + emitEvent?: boolean; + }): void; + contains(controlName: K): boolean; + // (undocumented) + contains(this: FormGroup<{ + [key: string]: AbstractControl; + }>, controlName: string): boolean; + // (undocumented) + controls: ɵTypedOrUntyped; + }>; + getRawValue(): ɵTypedOrUntyped, any>; + patchValue(value: ɵFormGroupValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - registerControl(name: string, control: AbstractControl): AbstractControl; - removeControl(name: string, options?: { + registerControl(name: K, control: TControl[K]): TControl[K]; + // (undocumented) + registerControl(this: FormGroup<{ + [key: string]: AbstractControl; + }>, name: string, control: AbstractControl): AbstractControl; + // (undocumented) + removeControl(this: FormGroup<{ + [key: string]: AbstractControl; + }>, name: string, options?: { + emitEvent?: boolean; + }): void; + // (undocumented) + removeControl(name: ɵOptionalKeys & S, options?: { emitEvent?: boolean; }): void; - reset(value?: any, options?: { + reset(value?: ɵTypedOrUntyped, any>, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; - setControl(name: string, control: AbstractControl, options?: { + setControl(name: K, control: TControl[K], options?: { emitEvent?: boolean; }): void; - setValue(value: { - [key: string]: any; - }, options?: { + // (undocumented) + setControl(this: FormGroup<{ + [key: string]: AbstractControl; + }>, name: string, control: AbstractControl, options?: { + emitEvent?: boolean; + }): void; + setValue(value: ɵFormGroupRawValue, options?: { onlySelf?: boolean; emitEvent?: boolean; }): void; @@ -724,7 +775,7 @@ export class SelectMultipleControlValueAccessor extends BuiltInControlValueAcces } // @public -export type UntypedFormArray = FormArray; +export type UntypedFormArray = FormArray; // @public (undocumented) export const UntypedFormArray: UntypedFormArrayCtor; @@ -752,13 +803,13 @@ export class UntypedFormBuilder extends FormBuilder { } // @public -export type UntypedFormControl = FormControl; +export type UntypedFormControl = FormControl; // @public (undocumented) export const UntypedFormControl: UntypedFormControlCtor; // @public -export type UntypedFormGroup = FormGroup; +export type UntypedFormGroup = FormGroup; // @public (undocumented) export const UntypedFormGroup: UntypedFormGroupCtor; diff --git a/modules/playground/src/model_driven_forms/index.ts b/modules/playground/src/model_driven_forms/index.ts index bc980729a54e2..33e4c735fa9db 100644 --- a/modules/playground/src/model_driven_forms/index.ts +++ b/modules/playground/src/model_driven_forms/index.ts @@ -8,7 +8,7 @@ /* tslint:disable:no-console */ import {Component, Host, NgModule} from '@angular/core'; -import {AbstractControl, FormBuilder, FormGroup, FormGroupDirective, ReactiveFormsModule, Validators} from '@angular/forms'; +import {AbstractControl, FormBuilder, FormGroup, FormGroupDirective, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -139,10 +139,10 @@ export class ShowError { ` }) export class ReactiveForms { - form: FormGroup; + form: UntypedFormGroup; countries = ['US', 'Canada']; - constructor(fb: FormBuilder) { + constructor(fb: UntypedFormBuilder) { this.form = fb.group({ 'firstName': ['', Validators.required], 'middleName': [''], diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 5dbacf1cd381d..651efe79f9dfc 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -6,8 +6,8 @@ "factory": "./migrations/entry-components/index" }, "migration-v14-typed-forms": { - "version": "9999.0.0", - "description": "Experimental migration that adds s for Typed Forms.", + "version": "14.0.0-beta", + "description": "As of Angular version 14, Forms model classes accept a type parameter, and existing usages must be opted out to preserve backwards-compatibility.", "factory": "./migrations/typed-forms/index" }, "migration-v14-path-match-type": { 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 2d6dfbdb11d64..fc32acb326889 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -1095,7 +1095,7 @@ "name": "isEmptyInputValue" }, { - "name": "isFormControl" + "name": "isFormControlState" }, { "name": "isForwardRef" @@ -1353,7 +1353,7 @@ "name": "removeFromArray" }, { - "name": "removeListItem" + "name": "removeListItem2" }, { "name": "removeStyle" 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 61923181412f0..312aa340e6330 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 @@ -1058,6 +1058,9 @@ { "name": "isDirectiveHost" }, + { + "name": "isFormControlState" + }, { "name": "isForwardRef" }, diff --git a/packages/forms/src/directives/control_container.ts b/packages/forms/src/directives/control_container.ts index 4c47e42b90d68..9c88665dd4a0d 100644 --- a/packages/forms/src/directives/control_container.ts +++ b/packages/forms/src/directives/control_container.ts @@ -9,6 +9,7 @@ import {AbstractControlDirective} from './abstract_control_directive'; import {Form} from './form_interface'; + /** * @description * A base class for directives that contain multiple registered instances of `NgControl`. diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index c66702609390e..0bf4f95b148e2 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -11,12 +11,11 @@ import {Directive, EventEmitter, forwardRef, Inject, Input, OnChanges, OnDestroy import {FormArray} from '../../model/form_array'; import {FormControl, isFormControl} from '../../model/form_control'; import {FormGroup} from '../../model/form_group'; -import {removeListItem} from '../../util'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {ControlContainer} from '../control_container'; import {Form} from '../form_interface'; import {missingFormException} from '../reactive_errors'; -import {cleanUpControl, cleanUpFormContainer, cleanUpValidators, setUpControl, setUpFormContainer, setUpValidators, syncPendingControls} from '../shared'; +import {cleanUpControl, cleanUpFormContainer, cleanUpValidators, removeListItem, setUpControl, setUpFormContainer, setUpValidators, syncPendingControls} from '../shared'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators'; import {FormControlName} from './form_control_name'; diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index a2b35e884d653..1373f7171de08 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -358,6 +358,11 @@ export function selectValueAccessor( return null; } +export function removeListItem(list: T[], el: T): void { + const index = list.indexOf(el); + if (index > -1) list.splice(index, 1); +} + // TODO(kara): remove after deprecation period export function _ngModelWarning( name: string, type: {_ngModelWarningSentOnce: boolean}, diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index 6f602b19c9e3a..165869645e65f 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -11,9 +11,9 @@ import {Injectable} from '@angular/core'; import {AsyncValidatorFn, ValidatorFn} from './directives/validators'; import {ReactiveFormsModule} from './form_providers'; import {AbstractControl, AbstractControlOptions, FormHooks} from './model/abstract_model'; -import {FormArray, isFormArray, UntypedFormArray} from './model/form_array'; -import {FormControl, FormControlOptions, isFormControl, UntypedFormControl} from './model/form_control'; -import {FormGroup, isFormGroup, UntypedFormGroup} from './model/form_group'; +import {FormArray, UntypedFormArray} from './model/form_array'; +import {FormControl, FormControlOptions, FormControlState, UntypedFormControl} from './model/form_control'; +import {FormGroup, UntypedFormGroup} from './model/form_group'; function isAbstractControlOptions(options: AbstractControlOptions| {[key: string]: any}): options is AbstractControlOptions { @@ -22,38 +22,49 @@ function isAbstractControlOptions(options: AbstractControlOptions| (options).updateOn !== undefined; } +/** + * ControlConfig is a tuple containing a value of type T, plus optional validators and async + * validators. + * + * @publicApi + */ +export type ControlConfig = [T|FormControlState, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?]; + +// Disable clang-format to produce clearer formatting for this multiline type. +// clang-format off +export type ɵGroupElement = + T extends FormControl ? FormControl : + T extends FormGroup ? FormGroup : + T extends FormArray ? FormArray : + T extends AbstractControl ? AbstractControl : + T extends FormControlState ? FormControl : + T extends ControlConfig ? FormControl : + FormControl; + +// clang-format on + /** * @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. + * 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) + * @see [Reactive Forms Guide](guide/reactive-forms) * * @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}, + group| ControlConfig| FormControl| FormGroup| + FormArray| AbstractControl| T[K] + }>( + controls: T, options?: AbstractControlOptions|null, - ): FormGroup; + ): FormGroup<{[K in keyof T]: ɵGroupElement}>; + /** * @description * Construct a new `FormGroup` instance. @@ -62,35 +73,50 @@ export class FormBuilder { * 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) => + * `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 controls A record of child controls. The key for each child is the name + * under which the control 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 + * * `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}, + controls: {[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); + + /** + * @description + * Construct a new `FormGroup` instance. + * + * @param controls 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 + * `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(controls: {[key: string]: any}, options: AbstractControlOptions|{[key: string]: + any}|null = null): + FormGroup { + const reducedControls = this._reduceControls(controls); let validators: ValidatorFn|ValidatorFn[]|null = null; let asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null = null; let updateOn: FormHooks|undefined = undefined; - if (options != null) { + if (options !== null) { if (isAbstractControlOptions(options)) { // `options` are `AbstractControlOptions` validators = options.validators != null ? options.validators : null; @@ -103,18 +129,29 @@ export class FormBuilder { } } - return new FormGroup(controls, {asyncValidators, updateOn, validators}); + return new FormGroup(reducedControls, {asyncValidators, updateOn, validators}); } + control(formState: T|FormControlState, opts: FormControlOptions&{ + initialValueIsDefault: true + }): FormControl; + + control( + formState: T|FormControlState, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; + /** * @description - * Construct a new `FormControl` with the given state, validators and options. + * 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 `AbstractControlOptions` object that contains + * 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 @@ -129,56 +166,85 @@ export class FormBuilder { * * */ - control( - formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl { + control( + formState: T|FormControlState, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| + null): FormControl|FormControl { return new FormControl(formState, validatorOrOpts, asyncValidator); } + array( + controls: Array>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray>; + + array}>( + controls: Array>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray>; + + array>( + controls: Array>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray>; + + array>( + controls: Array>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray>; + + array( + controls: Array|ControlConfig|T>, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray>; + /** * 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 controls 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 + * @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. + * @param asyncValidator A single async validator or array of async validator functions. */ array( - controlsConfig: any[], - validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + controls: 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); + const createdControls = controls.map(c => this._createControl(c)); + return new FormArray(createdControls, 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]); + _reduceControls(controls: + {[k: string]: T|ControlConfig|FormControlState|AbstractControl}): + {[key: string]: AbstractControl} { + const createdControls: {[key: string]: AbstractControl} = {}; + Object.keys(controls).forEach(controlName => { + createdControls[controlName] = this._createControl(controls[controlName]); }); - return controls; + return createdControls; } /** @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); + _createControl(controls: T|FormControlState|ControlConfig|FormControl| + AbstractControl): FormControl|FormControl|AbstractControl { + if (controls instanceof FormControl) { + return controls as FormControl; + } else if (controls instanceof AbstractControl) { // A control; just return it + return controls; + } else if (Array.isArray(controls)) { // ControlConfig Tuple + const value: T|FormControlState = controls[0]; + const validator: ValidatorFn|ValidatorFn[]|null = controls.length > 1 ? controls[1]! : null; + const asyncValidator: AsyncValidatorFn|AsyncValidatorFn[]|null = + controls.length > 2 ? controls[2]! : null; + return this.control(value, validator, asyncValidator); + } else { // T or FormControlState + return this.control(controls); } } } diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 604152ccc39ce..5152e22857d3d 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -41,11 +41,11 @@ export {FormArrayName, FormGroupName} from './directives/reactive_directives/for export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor'; export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; -export {FormBuilder, UntypedFormBuilder} from './form_builder'; -export {AbstractControl, AbstractControlOptions, FormControlStatus} from './model/abstract_model'; -export {FormArray, UntypedFormArray} from './model/form_array'; -export {FormControl, FormControlOptions, UntypedFormControl, ɵFormControlCtor} from './model/form_control'; -export {FormGroup, UntypedFormGroup} from './model/form_group'; +export {FormBuilder, UntypedFormBuilder, ɵGroupElement} from './form_builder'; +export {AbstractControl, AbstractControlOptions, FormControlStatus, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model'; +export {FormArray, UntypedFormArray, ɵFormArrayRawValue, ɵFormArrayValue} from './model/form_array'; +export {FormControl, FormControlOptions, FormControlState, UntypedFormControl, ɵFormControlCtor} from './model/form_control'; +export {FormGroup, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; export {VERSION} from './version'; diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index f653b53f91e46..5a3f8ddf827ab 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -18,22 +18,22 @@ import {addValidators, composeAsyncValidators, composeValidators, hasValidator, const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode; /** - * Constant indicating that a control is valid, meaning that no errors exist in the input value. + * Reports that a control is valid, meaning that no errors exist in the input value. * * @see `status` */ export const VALID = 'VALID'; /** - * Constant indicating that a control is invalid, meaning that an error exists in the input value. + * Reports that a control is invalid, meaning that an error exists in the input value. * * @see `status` */ export const INVALID = 'INVALID'; /** - * Constant indicating that a control is pending, meaning that that async validation is occurring - * and errors are not yet available for the input value. + * Reports that a control is pending, meaning that that async validation is occurring and + * errors are not yet available for the input value. * * @see `markAsPending` * @see `status` @@ -41,7 +41,7 @@ export const INVALID = 'INVALID'; export const PENDING = 'PENDING'; /** - * Constant indicating that a control is disabled, meaning that the control is exempt from ancestor + * Reports that a control is disabled, meaning that the control is exempt from ancestor * calculations of validity or value. * * @see `markAsDisabled` @@ -53,13 +53,13 @@ export const DISABLED = 'DISABLED'; * A form can have several different statuses. Each * possible status is returned as a string literal. * - * * **VALID**: Reports that a FormControl is valid, meaning that no errors exist in the input + * * **VALID**: Reports that a control is valid, meaning that no errors exist in the input * value. - * * **INVALID**: Reports that a FormControl is invalid, meaning that an error exists in the input + * * **INVALID**: Reports that a control is invalid, meaning that an error exists in the input * value. - * * **PENDING**: Reports that a FormControl is pending, meaning that that async validation is + * * **PENDING**: Reports that a control is pending, meaning that that async validation is * occurring and errors are not yet available for the input value. - * * **DISABLED**: Reports that a FormControl is + * * **DISABLED**: Reports that a control is * disabled, meaning that the control is exempt from ancestor calculations of validity or value. * * @publicApi @@ -145,15 +145,110 @@ export function assertControlPresent(parent: any, isGroup: boolean, key: string| } export function assertAllValuesPresent(control: any, isGroup: boolean, value: any): void { - control._forEachChild(((_: unknown, key: string|number) => { - if (value[key] === undefined) { - throw new RuntimeError( - RuntimeErrorCode.MISSING_CONTROL_VALUE, - NG_DEV_MODE ? missingControlValueError(isGroup, key) : ''); - } - }) as any); + control._forEachChild((_: unknown, key: string|number) => { + if (value[key] === undefined) { + throw new RuntimeError( + RuntimeErrorCode.MISSING_CONTROL_VALUE, + NG_DEV_MODE ? missingControlValueError(isGroup, key) : ''); + } + }); } +// IsAny checks if T is `any`, by checking a condition that couldn't possibly be true otherwise. +export type ɵIsAny = 0 extends(1&T) ? Y : N; + +/** + * `TypedOrUntyped` allows one of two different types to be selected, depending on whether the Forms + * class it's applied to is typed or not. + * + * This is for internal Angular usage to support typed forms; do not directly use it. + */ +export type ɵTypedOrUntyped = ɵIsAny; + +/** + * Value gives the type of `.value` in an `AbstractControl`. + * + * For internal use only. + */ +export type ɵValue = + T extends AbstractControl? T['value'] : never; + +/** + * RawValue gives the type of `.getRawValue()` in an `AbstractControl`. + * + * For internal use only. + */ +export type ɵRawValue = T extends AbstractControl? + (T['setValue'] extends((v: infer R) => void) ? R : never) : + never; + +// Disable clang-format to produce clearer formatting for these multiline types. +// clang-format off + +/** +* Tokenize splits a string literal S by a delimeter D. +*/ +export type ɵTokenize = + string extends S ? string[] : /* S must be a literal */ + S extends `${infer T}${D}${infer U}` ? [T, ...ɵTokenize] : + [S] /* Base case */ + ; + +/** +* CoerceStrArrToNumArr accepts an array of strings, and converts any numeric string to a number. +*/ +export type ɵCoerceStrArrToNumArr = + // Extract the head of the array. + S extends [infer Head, ...infer Tail] ? + // Using a template literal type, coerce the head to `number` if possible. + // Then, recurse on the tail. + Head extends `${number}` ? + [number, ...ɵCoerceStrArrToNumArr] : + [Head, ...ɵCoerceStrArrToNumArr] : + []; + +/** +* Navigate takes a type T and an array K, and returns the type of T[K[0]][K[1]][K[2]]... +*/ +export type ɵNavigate)> = + T extends object ? /* T must be indexable (object or array) */ + (K extends [infer Head, ...infer Tail] ? /* Split K into head and tail */ + (Head extends keyof T ? /* head(K) must index T */ + (Tail extends(string|number)[] ? /* tail(K) must be an array */ + [] extends Tail ? T[Head] : /* base case: K can be split, but Tail is empty */ + (ɵNavigate) /* explore T[head(K)] by tail(K) */ : + any) /* tail(K) was not an array, give up */ : + never) /* head(K) does not index T, give up */ : + any) /* K cannot be split, give up */ : + any /* T is not indexable, give up */ + ; + +/** + * ɵWriteable removes readonly from all keys. + */ +export type ɵWriteable = { + -readonly[P in keyof T]: T[P] +}; + +/** + * GetProperty takes a type T and 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. + * + * For internal use only. + */ +export type ɵGetProperty = + // K is a string + K extends string ? ɵGetProperty>> : + // Is is an array + ɵWriteable extends Array ? ɵNavigate> : + // Fall through permissively if we can't calculate the type of K. + any; + +// clang-format on + /** * This is the base class for `FormControl`, `FormGroup`, and `FormArray`. * @@ -162,13 +257,16 @@ export function assertAllValuesPresent(control: any, isGroup: boolean, value: an * that are shared between all sub-classes, like `value`, `valid`, and `dirty`. It shouldn't be * instantiated directly. * + * The first type parameter TValue represents the value type of the control (`control.value`). + * The optional type parameter TRawValue represents the raw value type (`control.getRawValue()`). + * * @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; @@ -239,7 +337,7 @@ export abstract class AbstractControl { * * For a `FormArray`, the values of enabled controls as an array. * */ - public readonly value: any; + public readonly value!: TValue; /** * Initialize the AbstractControl instance. @@ -412,7 +510,7 @@ 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; + public readonly valueChanges!: Observable; /** * A multicasting observable that emits an event every time the validation `status` of the control @@ -737,7 +835,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); } @@ -779,7 +877,7 @@ export abstract class AbstractControl { } private _updateAncestors( - opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}) { + opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}): void { if (this._parent && !opts.onlySelf) { this._parent.updateValueAndValidity(opts); if (!opts.skipPristineCheck) { @@ -790,26 +888,28 @@ 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: FormGroup|FormArray|null): void { this._parent = parent; } /** * Sets the value of the control. Abstract method (implemented in sub-classes). */ - abstract setValue(value: any, options?: Object): void; + abstract setValue(value: TRawValue, 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: TValue, options?: Object): void; /** * Resets the control. Abstract method (implemented in sub-classes). */ - abstract reset(value?: any, options?: Object): void; + abstract reset(value?: TValue, options?: Object): void; /** * The raw value of this control. For most control implementations, the raw value will include @@ -848,7 +948,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); } @@ -858,7 +958,7 @@ export abstract class AbstractControl { } /** @internal */ - _updateTreeValidity(opts: {emitEvent?: boolean} = {emitEvent: true}) { + _updateTreeValidity(opts: {emitEvent?: boolean} = {emitEvent: true}): void { this._forEachChild((ctrl: AbstractControl) => ctrl._updateTreeValidity(opts)); this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); } @@ -921,11 +1021,30 @@ 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<ɵGetProperty>|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<ɵGetProperty>|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 @@ -936,7 +1055,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 * @@ -949,11 +1068,13 @@ export abstract class AbstractControl { * * * `this.form.get(['items', 0, 'price']);` */ - get(path: Array|string): AbstractControl|null { - if (path == null) return null; - if (!Array.isArray(path)) path = path.split('.'); - if (path.length === 0) return null; - return path.reduce( + get

(path: P): + AbstractControl<ɵGetProperty>|null { + let currPath: Array|string = path; + if (currPath == null) return null; + if (!Array.isArray(currPath)) currPath = currPath.split('.'); + if (currPath.length === 0) return null; + return currPath.reduce( (control: AbstractControl|null, name) => control && control._find(name), this); } @@ -1051,7 +1172,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(); } @@ -1115,12 +1236,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; diff --git a/packages/forms/src/model/form_array.ts b/packages/forms/src/model/form_array.ts index 205011ec3132f..ad9315e465c28 100644 --- a/packages/forms/src/model/form_array.ts +++ b/packages/forms/src/model/form_array.ts @@ -8,7 +8,26 @@ import {AsyncValidatorFn, ValidatorFn} from '../directives/validators'; -import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators} from './abstract_model'; +import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, ɵRawValue, ɵTypedOrUntyped, ɵValue} from './abstract_model'; + +/** + * FormArrayValue extracts the type of `.value` from a FormArray's element type, and wraps it in an + * array. + * + * Angular uses this type internally to support Typed Forms; do not use it directly. The untyped + * case falls back to any[]. + */ +export type ɵFormArrayValue> = + ɵTypedOrUntyped>, any[]>; + +/** + * FormArrayRawValue extracts the type of `.getRawValue()` from a FormArray's element type, and + * wraps it in an array. The untyped case falls back to any[]. + * + * Angular uses this type internally to support Typed Forms; do not use it directly. + */ +export type ɵFormArrayRawValue> = + ɵTypedOrUntyped>, any[]>; /** * Tracks the value and validity state of an array of `FormControl`, @@ -74,7 +93,9 @@ import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertC * * @publicApi */ -export class FormArray extends AbstractControl { +export class FormArray = any> extends AbstractControl< + ɵTypedOrUntyped, any>, + ɵTypedOrUntyped, any>> { /** * Creates a new `FormArray` instance. * @@ -89,10 +110,11 @@ export class FormArray extends AbstractControl { * */ constructor( - public controls: AbstractControl[], + controls: Array, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); + this.controls = controls; this._initObservables(); this._setUpdateStrategy(validatorOrOpts); this._setUpControls(); @@ -106,6 +128,8 @@ export class FormArray extends AbstractControl { }); } + public controls: ɵTypedOrUntyped, Array>>; + /** * Get the `AbstractControl` at the given `index` in the array. * @@ -113,8 +137,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: number): ɵTypedOrUntyped> { + return (this.controls as any)[this._adjustIndex(index)]; } /** @@ -127,7 +151,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: TControl, options: {emitEvent?: boolean} = {}): void { this.controls.push(control); this._registerControl(control); this.updateValueAndValidity({emitEvent: options.emitEvent}); @@ -147,7 +171,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: TControl, options: {emitEvent?: boolean} = {}): void { this.controls.splice(index, 0, control); this._registerControl(control); @@ -190,7 +214,7 @@ 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: TControl, 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; @@ -250,7 +274,10 @@ 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, false, value); value.forEach((newValue: any, index: number) => { assertControlPresent(this, false, index); @@ -287,18 +314,21 @@ 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}); } @@ -352,7 +382,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: ɵTypedOrUntyped, any> = [], options: { + onlySelf?: boolean, + emitEvent?: boolean + } = {}): void { this._forEachChild((control: AbstractControl, index: number) => { control.reset(value[index], {onlySelf: true, emitEvent: options.emitEvent}); }); @@ -365,9 +398,8 @@ 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. */ - override getRawValue(): any[] { + override getRawValue(): ɵFormArrayRawValue { return this.controls.map((control: AbstractControl) => control.getRawValue()); } @@ -409,14 +441,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 { @@ -425,7 +457,7 @@ export class FormArray extends AbstractControl { /** @internal */ override _syncPendingControls(): boolean { - let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => { + let subtreeUpdated = (this.controls as any).reduce((updated: any, child: any) => { return child._syncPendingControls() ? true : updated; }, false); if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true}); @@ -448,12 +480,12 @@ export class FormArray extends AbstractControl { /** @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 */ @@ -484,7 +516,7 @@ interface UntypedFormArrayCtor { * The presence of an explicit `prototype` property provides backwards-compatibility for apps that * manually inspect the prototype chain. */ - prototype: FormArray; + prototype: FormArray; } /** @@ -492,7 +524,7 @@ interface UntypedFormArrayCtor { * Note: this is used for migration purposes only. Please avoid using it directly in your code and * prefer `FormControl` instead, unless you have been migrated to it automatically. */ -export type UntypedFormArray = FormArray; +export type UntypedFormArray = FormArray; export const UntypedFormArray: UntypedFormArrayCtor = FormArray; diff --git a/packages/forms/src/model/form_control.ts b/packages/forms/src/model/form_control.ts index dbd0c5611f9b6..e611868c6f940 100644 --- a/packages/forms/src/model/form_control.ts +++ b/packages/forms/src/model/form_control.ts @@ -12,7 +12,17 @@ import {removeListItem} from '../util'; import {AbstractControl, AbstractControlOptions, isOptionsObj, pickAsyncValidators, pickValidators} from './abstract_model'; /** - * Interface for options provided to a {@link FormControl}. + * FormControlState is a boxed form value. It is an object with a `value` key and a `disabled` key. + * + * @publicApi + */ +export interface FormControlState { + value: T; + disabled: boolean; +} + +/** + * Interface for options provided to a `FormControl`. * * This interface extends all options from {@link AbstractControlOptions}, plus some options * unique to `FormControl`. @@ -22,7 +32,7 @@ import {AbstractControl, AbstractControlOptions, isOptionsObj, pickAsyncValidato 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. @@ -129,23 +139,22 @@ export interface FormControlOptions extends AbstractControlOptions { * 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: TValue; /** @internal */ _onChange: Function[]; /** * This field holds a pending value that has not yet been applied to the form's value. - * It is `any` because the value is untyped. * @internal */ - _pendingValue: any; + _pendingValue: TValue; /** @internal */ _pendingChange: boolean; @@ -173,7 +182,7 @@ export interface FormControl extends AbstractControl { * event to update the model. * */ - setValue(value: any, options?: { + setValue(value: TValue, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -189,7 +198,7 @@ export interface FormControl extends AbstractControl { * * @see `setValue` for options */ - patchValue(value: any, options?: { + patchValue(value: TValue, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -229,7 +238,15 @@ export interface FormControl extends AbstractControl { * When false, no events are emitted. * */ - reset(formState?: any, options?: {onlySelf?: boolean, emitEvent?: boolean}): void; + reset(formState?: TValue|FormControlState, options?: { + onlySelf?: boolean, + emitEvent?: boolean + }): void; + + /** + * For a simple FormControl, the raw value is equivalent to the value. + */ + getRawValue(): TValue; /** * @internal @@ -283,7 +300,9 @@ export interface FormControl extends AbstractControl { _syncPendingControls(): boolean; } -type FormControlInterface = FormControl; +// This internal interface is present to avoid a naming clash, resulting in the wrong `FormControl` +// symbol being used. +type FormControlInterface = FormControl; /** * Various available constructors for `FormControl`. @@ -297,7 +316,7 @@ export interface ɵFormControlCtor { /** * Construct a FormControl with no initial value or validators. */ - new(): FormControl; + new(): FormControl; /** * Creates a new `FormControl` instance. @@ -309,34 +328,46 @@ 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: FormControlState|T, opts: FormControlOptions&{ + initialValueIsDefault: true + }): FormControl; + new( + value: FormControlState|T, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; /** * The presence of an explicit `prototype` property provides backwards-compatibility for apps that * manually inspect the prototype chain. */ - prototype: FormControl; + prototype: FormControl; +} + +function isFormControlState(formState: unknown): formState is FormControlState { + 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< + TValue> implements FormControlInterface { /** @publicApi */ - public readonly defaultValue: any = null; + public readonly defaultValue: TValue = null as unknown as TValue; /** @internal */ - _onChange: Function[] = []; + _onChange: Array = []; /** @internal */ - _pendingValue: any; + _pendingValue!: TValue; /** @internal */ _pendingChange: boolean = false; constructor( - formState: any = null, + // formState and defaultValue will only be null if T is nullable + formState: FormControlState|TValue = null as unknown as TValue, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super( @@ -353,21 +384,21 @@ export const FormControl: ɵFormControlCtor = emitEvent: !!this.asyncValidator }); if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) { - if (this._isBoxedValue(formState)) { - (this.defaultValue as any) = formState.value; + if (isFormControlState(formState)) { + this.defaultValue = formState.value; } else { - (this.defaultValue as any) = formState; + this.defaultValue = formState; } } } - override setValue(value: any, options: { + override setValue(value: TValue, options: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, emitViewToModelChange?: boolean } = {}): void { - (this as {value: any}).value = this._pendingValue = value; + (this as {value: TValue}).value = this._pendingValue = value; if (this._onChange.length && options.emitModelToViewChange !== false) { this._onChange.forEach( (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false)); @@ -375,7 +406,7 @@ export const FormControl: ɵFormControlCtor = this.updateValueAndValidity(options); } - override patchValue(value: any, options: { + override patchValue(value: TValue, options: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, @@ -385,7 +416,7 @@ export const FormControl: ɵFormControlCtor = } override reset( - formState: any = this.defaultValue, + formState: TValue|FormControlState = this.defaultValue, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { this._applyFormState(formState); this.markAsPristine(options); @@ -441,13 +472,13 @@ 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: FormControlState|TValue) { + if (isFormControlState(formState)) { + (this as {value: TValue}).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: TValue}).value = this._pendingValue = formState; } } }); @@ -462,7 +493,7 @@ interface UntypedFormControlCtor { * The presence of an explicit `prototype` property provides backwards-compatibility for apps that * manually inspect the prototype chain. */ - prototype: FormControl; + prototype: FormControl; } /** @@ -470,7 +501,7 @@ interface UntypedFormControlCtor { * Note: this is used for migration purposes only. Please avoid using it directly in your code and * prefer `FormControl` instead, unless you have been migrated to it automatically. */ -export type UntypedFormControl = FormControl; +export type UntypedFormControl = FormControl; export const UntypedFormControl: UntypedFormControlCtor = FormControl; diff --git a/packages/forms/src/model/form_group.ts b/packages/forms/src/model/form_group.ts index 1037e040f8b32..da7d8c0ab1c77 100644 --- a/packages/forms/src/model/form_group.ts +++ b/packages/forms/src/model/form_group.ts @@ -8,7 +8,38 @@ import {AsyncValidatorFn, ValidatorFn} from '../directives/validators'; -import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators} from './abstract_model'; +import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, ɵRawValue, ɵTypedOrUntyped, ɵValue} from './abstract_model'; + +/** + * FormGroupValue extracts the type of `.value` from a FormGroup's inner object type. The untyped + * case falls back to {[key: string]: any}. + * + * Angular uses this type internally to support Typed Forms; do not use it directly. + * + * For internal use only. + */ +export type ɵFormGroupValue}> = + ɵTypedOrUntyped}>, {[key: string]: any}>; + +/** + * FormGroupRawValue extracts the type of `.getRawValue()` from a FormGroup's inner object type. The + * untyped case falls back to {[key: string]: any}. + * + * Angular uses this type internally to support Typed Forms; do not use it directly. + * + * For internal use only. + */ +export type ɵFormGroupRawValue}> = + ɵTypedOrUntyped}, {[key: string]: any}>; + +/** + * OptionalKeys returns the union of all optional keys in the object. + * + * Angular uses this type internally to support Typed Forms; do not use it directly. + */ +export type ɵOptionalKeys = { + [K in keyof T] -?: undefined extends T[K] ? K : never +}[keyof T]; /** * Tracks the value and validity state of a group of `FormControl` instances. @@ -24,6 +55,9 @@ import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertC * 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 `TControl`, which is an object type with inner + * control types as values. + * * @usageNotes * * ### Create a form group with 2 controls @@ -80,9 +114,27 @@ import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertC * }, { 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} = any> extends + AbstractControl< + ɵTypedOrUntyped, any>, + ɵTypedOrUntyped, any>> { /** * Creates a new `FormGroup` instance. * @@ -97,10 +149,10 @@ export class FormGroup extends AbstractControl { * */ constructor( - public controls: {[key: string]: AbstractControl}, - validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + controls: TControl, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); + this.controls = controls; this._initObservables(); this._setUpdateStrategy(validatorOrOpts); this._setUpControls(); @@ -113,8 +165,11 @@ export class FormGroup extends AbstractControl { }); } + public controls: ɵTypedOrUntyped}>; + /** - * 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. @@ -122,16 +177,22 @@ 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 { - if (this.controls[name]) return this.controls[name]; + registerControl(name: K, control: TControl[K]): TControl[K]; + registerControl( + this: FormGroup<{[key: string]: AbstractControl}>, name: string, + control: AbstractControl): AbstractControl; + + registerControl(name: K, control: TControl[K]): TControl[K] { + if (this.controls[name]) return (this.controls as any)[name]; this.controls[name] = control; - control.setParent(this); + control.setParent(this as FormGroup); control._registerOnCollectionChange(this._onCollectionChange); return control; } /** - * 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} @@ -145,14 +206,31 @@ 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( + this: FormGroup<{[key: string]: AbstractControl}>, name: string, + control: AbstractControl, options?: {emitEvent?: boolean}): void; + addControl(name: K, control: Required[K], 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(); } + removeControl(this: FormGroup<{[key: string]: AbstractControl}>, name: string, options?: { + emitEvent?: boolean; + }): void; + removeControl(name: ɵOptionalKeys&S, options?: { + emitEvent?: boolean; + }): void; + /** - * 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. * @@ -163,15 +241,17 @@ 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: string, options: {emitEvent?: boolean;} = {}): void { + if ((this.controls as any)[name]) + (this.controls as any)[name]._registerOnCollectionChange(() => {}); + delete ((this.controls as any)[name]); 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. * @@ -183,7 +263,16 @@ 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: TControl[K], options?: { + emitEvent?: boolean + }): void; + setControl( + this: FormGroup<{[key: string]: AbstractControl}>, name: string, + control: AbstractControl, options?: {emitEvent?: boolean}): void; + + setControl(name: K, control: TControl[K], options: { + emitEvent?: boolean + } = {}): void { if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {}); delete (this.controls[name]); if (control) this.registerControl(name, control); @@ -201,7 +290,10 @@ export class FormGroup extends AbstractControl { * * @returns false for disabled controls, true otherwise. */ - contains(controlName: string): boolean { + contains(controlName: K): boolean; + contains(this: FormGroup<{[key: string]: AbstractControl}>, controlName: string): boolean; + + contains(controlName: K): boolean { return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled; } @@ -240,12 +332,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, true, value); - Object.keys(value).forEach(name => { - assertControlPresent(this, true, name); - this.controls[name].setValue(value[name], {onlySelf: true, emitEvent: options.emitEvent}); + (Object.keys(value) as Array).forEach(name => { + assertControlPresent(this, true, name as any); + (this.controls as any)[name].setValue( + (value as any)[name], {onlySelf: true, emitEvent: options.emitEvent}); }); this.updateValueAndValidity(options); } @@ -281,17 +376,24 @@ export class FormGroup extends AbstractControl { * is updated. When false, no events are emitted. The configuration options are passed to * the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method. */ - override patchValue( - value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + override patchValue(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 => { - if (this.controls[name]) { - this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent: options.emitEvent}); + (Object.keys(value) as Array).forEach(name => { + // The compiler cannot see through the uninstantiated conditional type of `this.controls`, so + // `as any` is required. + const control = (this.controls as any)[name]; + if (control) { + control.patchValue( + /* Guaranteed to be present, due to the outer forEach. */ value + [name as keyof ɵFormGroupValue]!, + {onlySelf: true, emitEvent: options.emitEvent}); } }); this.updateValueAndValidity(options); @@ -299,7 +401,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 @@ -354,9 +456,12 @@ 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: ɵTypedOrUntyped, any> = {} 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); @@ -367,20 +472,17 @@ export class FormGroup extends AbstractControl { * 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`. */ - override getRawValue(): any { - return this._reduceChildren( - {}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => { - acc[name] = control.getRawValue(); - return acc; - }); + override getRawValue(): ɵTypedOrUntyped, any> { + return this._reduceChildren({}, (acc, control, name) => { + (acc as any)[name] = (control as any).getRawValue(); + return acc; + }) as any; } /** @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}); @@ -388,19 +490,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); }); @@ -413,9 +515,8 @@ export class FormGroup extends AbstractControl { /** @internal */ override _anyControls(condition: (c: AbstractControl) => boolean): boolean { - for (const controlName of Object.keys(this.controls)) { - const control = this.controls[controlName]; - if (this.contains(controlName) && condition(control)) { + for (const [controlName, control] of Object.entries(this.controls)) { + if (this.contains(controlName as any) && condition(control as any)) { return true; } } @@ -423,20 +524,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: TControl[K], name: K) => T): T { let res = initValue; - this._forEachChild((control: AbstractControl, name: string) => { + this._forEachChild((control: TControl[K], name: K) => { res = fn(res, control, name); }); return res; @@ -444,8 +546,8 @@ export class FormGroup extends AbstractControl { /** @internal */ override _allControlsDisabled(): boolean { - for (const controlName of Object.keys(this.controls)) { - if (this.controls[controlName].enabled) { + for (const controlName of (Object.keys(this.controls) as Array)) { + if ((this.controls as any)[controlName].enabled) { return false; } } @@ -454,7 +556,9 @@ export class FormGroup extends AbstractControl { /** @internal */ override _find(name: string|number): AbstractControl|null { - return this.controls.hasOwnProperty(name as string) ? this.controls[name as string] : null; + return this.controls.hasOwnProperty(name as string) ? + (this.controls as any)[name as keyof TControl] : + null; } } @@ -467,7 +571,7 @@ interface UntypedFormGroupCtor { * The presence of an explicit `prototype` property provides backwards-compatibility for apps that * manually inspect the prototype chain. */ - prototype: FormGroup; + prototype: FormGroup; } /** @@ -475,7 +579,7 @@ interface UntypedFormGroupCtor { * Note: this is used for migration purposes only. Please avoid using it directly in your code and * prefer `FormControl` instead, unless you have been migrated to it automatically. */ -export type UntypedFormGroup = FormGroup; +export type UntypedFormGroup = FormGroup; export const UntypedFormGroup: UntypedFormGroupCtor = FormGroup; diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 17cdeb7c8972f..a9b05eda4e942 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -21,7 +21,7 @@ describe('FormArray', () => { let logger: string[]; beforeEach(() => { - a = new FormArray([]); + a = new FormArray([]); c1 = new FormControl(1); c2 = new FormControl(2); c3 = new FormControl(3); @@ -138,7 +138,7 @@ describe('FormArray', () => { // becomes invalid. const validatorFn = (value: any) => value.controls.length > 0 ? {controls: true} : null; const asyncValidatorFn = (value: any) => of(validatorFn(value)); - const arr: FormArray = new FormArray([], validatorFn, asyncValidatorFn); + const arr: FormArray = new FormArray([], validatorFn, asyncValidatorFn); expect(arr.valid).toBe(true); arr.statusChanges.subscribe(() => logger.push('status change')); @@ -309,7 +309,7 @@ describe('FormArray', () => { }); it('should throw if no controls are set yet', () => { - const empty: FormArray = new FormArray([]); + const empty: FormArray = new FormArray([]); expect(() => empty.setValue(['one'])) .toThrowError(new RegExp(`no form controls registered with this array`)); }); @@ -1177,7 +1177,7 @@ describe('FormArray', () => { }); it('should keep empty, disabled arrays disabled when updating validity', () => { - const arr: FormArray = new FormArray([]); + const arr: FormArray = new FormArray([]); expect(arr.status).toEqual('VALID'); arr.disable(); @@ -1524,6 +1524,22 @@ describe('FormArray', () => { expect(fa.value).toEqual([99, 1, 2, 3, 4]); }); }); + + describe('can be extended', () => { + it('by a simple strongly-typed array', () => { + abstract class StringFormArray extends FormArray { + override value!: string[]; + } + }); + + it('by a class that redefines many properties', () => { + abstract class OtherTypedFormArray< + TControls extends Array>> extends FormArray { + override controls!: TControls; + override value!: never[]; + } + }); + }); }); }); }); diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index 60074356ee57c..22907cc6d2379 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -2375,5 +2375,33 @@ describe('FormGroup', () => { }); }); }); + + describe('can be extended', () => { + it('by a group with string keys', () => { + abstract class StringKeyGroup extends FormGroup { + override registerControl(name: string, value: AbstractControl): AbstractControl { + return new FormControl(''); + } + } + }); + + it('by a group with generic keys', () => { + abstract class SpecialGroup extends FormGroup { + override registerControl( + name: K, value: AbstractControl): AbstractControl { + return new FormControl(''); + } + } + }); + + it('by a group with unconstrained generic keys', () => { + abstract class SpecialGroup extends FormGroup { + override registerControl( + name: K, value: AbstractControl): AbstractControl { + return new FormControl(''); + } + } + }); + }); }); })(); diff --git a/packages/forms/test/typed_integration_spec.ts b/packages/forms/test/typed_integration_spec.ts new file mode 100644 index 0000000000000..08040316fcc48 --- /dev/null +++ b/packages/forms/test/typed_integration_spec.ts @@ -0,0 +1,1291 @@ +/** + * @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 mainly check the types of strongly typed form controls, which is generally enforced +// at compile time. + +import {FormBuilder, UntypedFormBuilder} from '../src/form_builder'; +import {AbstractControl, FormArray, FormControl, FormGroup, UntypedFormArray, UntypedFormControl, UntypedFormGroup, Validators} from '../src/forms'; + +describe('Typed Class', () => { + describe('FormControl', () => { + it('supports 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(''); + // @ts-expect-error + c.setValue(null); + c.patchValue(''); + c.reset(''); + }); + + it('supports 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('supports explicit boolean controls', () => { + let c1: FormControl = new FormControl(false, {initialValueIsDefault: true}); + }); + + it('supports empty controls', () => { + let c = new FormControl(); + let ca: FormControl = c; + }); + + it('supports 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(''); + // @ts-expect-error + c.setValue(7); + c.patchValue(null); + c.patchValue(''); + c.reset(); + c.reset(''); + }); + + it('should create a nullable control without {initialValueIsDefault: true}', () => { + 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 not allow assignment to an incompatible control', () => { + let fcs = new FormControl('bob'); + let fcn = new FormControl(42); + // @ts-expect-error + fcs = fcn; + // @ts-expect-error + fcn = fcs; + }); + + it('is assignable to AbstractControl', () => { + let ac: AbstractControl; + ac = new FormControl(true, {initialValueIsDefault: true}); + }); + + it('is assignable to UntypedFormControl', () => { + const c = new FormControl(''); + let ufc: UntypedFormControl; + ufc = c; + }); + }); + + describe('FormGroup', () => { + it('supports 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.contains('foo'); // Contains checks always allowed + c.setValue({c: '', d: 0}); + c.patchValue({c: ''}); + c.reset({c: '', d: 0}); + }); + + it('supports 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('supports explicit groups with boolean types', () => { + const c0 = new FormGroup({a: new FormControl(true, {initialValueIsDefault: true})}); + + const c1: AbstractControl<{a?: boolean}, {a: boolean}> = + new FormGroup({a: new FormControl(true, {initialValueIsDefault: true})}); + + // const c2: FormGroup<{a: FormControl}> = + // new FormGroup({a: new FormControl(true, {initialValueIsDefault: true})}); + }); + + it('supports empty groups', () => { + let c = new FormGroup({}); + let ca: FormGroup = c; + }); + + it('supports 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('supports groups with the default type', () => { + let c: FormGroup; + let c2 = new FormGroup( + {c: new FormControl(''), d: new FormControl('', {initialValueIsDefault: true})}); + c = c2; + expect(c.value.d).toBe(''); + c.value; + c.reset(); + c.reset({c: ''}); + c.reset({c: '', d: ''}); + c.reset({c: '', d: ''}, {}); + c.setValue({c: '', d: ''}); + c.setValue({c: 99, d: 42}); + c.setControl('c', new FormControl(0)); + c.setControl('notpresent', new FormControl(0)); + c.removeControl('c'); + c.controls.d.valueChanges.subscribe((v) => {}); + }); + + it('supports groups with explicit named interface types', () => { + interface Cat { + lives: number; + } + interface CatControls { + lives: FormControl; + } + const c = + new FormGroup({lives: new FormControl(9, {initialValueIsDefault: true})}); + { + type ValueType = Partial; + let t: ValueType = c.value; + let t1 = c.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = Cat; + 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('supports groups with nested explicit named interface types', () => { + interface CatInterface { + name: string; + lives: number; + } + interface CatControlsInterface { + name: FormControl; + lives: FormControl; + } + + interface LitterInterface { + brother: CatInterface; + sister: CatInterface; + } + interface LitterControlsInterface { + brother: FormGroup; + sister: FormGroup; + } + const bro = new FormGroup({ + name: new FormControl('bob', {initialValueIsDefault: true}), + lives: new FormControl(9, {initialValueIsDefault: true}) + }); + const sis = new FormGroup({ + name: new FormControl('lucy', {initialValueIsDefault: true}), + lives: new FormControl(9, {initialValueIsDefault: true}) + }); + const litter = new FormGroup({ + brother: bro, + sister: sis, + }); + { + type ValueType = Partial<{brother: Partial, sister: Partial}>; + let t: ValueType = litter.value; + let t1 = litter.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = LitterInterface; + let t: RawValueType = litter.getRawValue(); + let t1 = litter.getRawValue(); + t1 = null as unknown as RawValueType; + } + litter.patchValue({brother: {name: 'jim'}}); + litter.controls.brother.setValue({name: 'jerry', lives: 1}); + }); + + it('supports 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('supports 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('supports 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('supports 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.setControl('c', new FormControl('', {initialValueIsDefault: true})); + c.contains('c'); + c.setValue({c: '', d: ''}); + c.patchValue({}); + c.reset({}); + c.reset({c: ''}); + c.reset({d: ''}); + c.reset({c: '', d: ''}); + // @ts-expect-error + c.removeControl('d'); // This is not allowed + }); + + it('supports nested groups with optional controls', () => { + type t = FormGroup<{meal: FormGroup<{dessert?: FormControl}>}>; + const menu = new FormGroup<{meal: FormGroup<{dessert?: FormControl}>}>( + {meal: new FormGroup({})}); + { + type ValueType = Partial<{meal: Partial<{dessert?: string}>}>; + let t: ValueType = menu.value; + let t1 = menu.value; + t1 = null as unknown as ValueType; + } + { + type RawValueType = {meal: {dessert?: string}}; + let t: RawValueType = menu.getRawValue(); + let t1 = menu.getRawValue(); + t1 = null as unknown as RawValueType; + } + menu.controls.meal.removeControl('dessert'); + }); + + it('supports 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})])); + // @ts-expect-error + c.registerControl('a', new FormArray([])); + 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})])); + // @ts-expect-error + 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})])); + // @ts-expect-error + 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('supports 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('supports groups with an index type', () => { + // This test is required for the default case, which relies on an index type with values + // AbstractControl. + 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.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'}); + // @ts-expect-error + c.removeControl('igor'); + }); + + 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; + } + { + // .get(...) should be `never`, but we use `?` to coerce to undefined so the test passes at + // runtime. + type ValueType = never|undefined; + let t: ValueType = c.get('foobar')?.value; + let t1 = c.get('foobar')?.value; + t1 = null as unknown as ValueType; + } + }); + + it('is assignable to AbstractControl', () => { + let ac: AbstractControl<{a?: boolean}>; + ac = new FormGroup({a: new FormControl(true, {initialValueIsDefault: true})}); + }); + + it('is assignable to UntypedFormGroup', () => { + let ufg: UntypedFormGroup; + const fg = new FormGroup({name: new FormControl('bob')}); + ufg = fg; + }); + + it('is assignable to UntypedFormGroup in a complex case', () => { + interface Cat { + name: FormControl; + lives?: FormControl; + } + let ufg: UntypedFormGroup; + const fg = new FormGroup( + {myCats: new FormArray([new FormGroup({name: new FormControl('bob')})])}); + ufg = fg; + }); + }); + + describe('FormArray', () => { + it('supports 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(); + c.valueChanges.subscribe(v => v); + }); + + it('supports 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('supports explicit arrays with boolean types', () => { + const c0 = new FormArray([new FormControl(true, {initialValueIsDefault: true})]); + + const c1: AbstractControl = + new FormArray([new FormControl(true, {initialValueIsDefault: true})]); + }); + + it('supports arrays with the default type', () => { + let c: FormArray; + 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.at(0).valueChanges.subscribe(v => {}); + 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('supports empty arrays', () => { + let fa = new FormArray([]); + }); + + it('supports 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('supports 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('supports 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('supports arrays with inferred nested groups', () => { + const fg = new FormGroup({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; + } + }); + + it('supports 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; + } + }); + + it('should have strongly-typed get', () => { + const c = new FormGroup({ + food: new FormArray([ + new FormControl('2200 Bryant', {initialValueIsDefault: true}), + ]) + }); + const rv = c.getRawValue(); + { + type ValueType = string[]; + let t: ValueType = c.get('food')!.value; + let t1 = c.get('food')!.value; + t1 = null as unknown as ValueType; + } + { + type ValueType = string; + let t: ValueType = c.get('food.0')!.value; + let t1 = c.get('food.0')!.value; + t1 = null as unknown as ValueType; + } + }); + + it('is assignable to UntypedFormArray', () => { + let ufa: UntypedFormArray; + const fa = new FormArray([new FormControl('bob')]); + ufa = fa; + }); + }); + + it('model classes support a complex, deeply nested case', () => { + interface Meal { + entree: FormControl; + dessert: FormControl; + } + const myParty = new FormGroup({ + 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}), + }), + }) + ]) + }); + { + 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 = myParty.value; + let t1 = myParty.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 = myParty.getRawValue(); + let t1 = myParty.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + + describe('FormBuilder', () => { + let fb: FormBuilder = new FormBuilder(); + + beforeEach(() => { + fb = new FormBuilder(); + }); + + describe('should work in basic cases', () => { + it('on FormControls', () => { + const fc = fb.control(42); + expect(fc.value).toEqual(42); + }); + + it('on FormGroups', () => { + const fc = fb.group({ + 'foo': 1, + 'bar': 2, + }); + expect(fc.value.foo).toEqual(1); + }); + }); + + describe('should build FormControls', () => { + it('nullably from values', () => { + const c = fb.control('foo'); + { + type RawValueType = string|null; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('non-nullably from values', () => { + const c = fb.control('foo', {initialValueIsDefault: true}); + { + type RawValueType = string; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('nullably from FormStates', () => { + const c = fb.control({value: 'foo', disabled: false}); + { + type RawValueType = string|null; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + + it('non-nullably from FormStates', () => { + const c = fb.control({value: 'foo', disabled: false}, {initialValueIsDefault: true}); + { + type RawValueType = string; + let t: RawValueType = c.getRawValue(); + let t1 = c.getRawValue(); + t1 = null as unknown as RawValueType; + } + }); + }); + + describe('should build FormGroups', () => { + it('from objects with plain values', () => { + const c = fb.group({foo: 'bar'}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with FormControlState', () => { + const c = fb.group({foo: {value: 'bar', disabled: false}}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with ControlConfigs', () => { + const c = fb.group({foo: ['bar']}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + describe('from objects with FormControls', () => { + it('nullably', () => { + const c = fb.group({foo: new FormControl('bar')}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('non-nullably', () => { + const c = fb.group({foo: new FormControl('bar', {initialValueIsDefault: true})}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with direct FormGroups', () => { + const c = fb.group({foo: new FormGroup({baz: new FormControl('bar')})}); + { + type ControlsType = {foo: FormGroup<{baz: FormControl}>}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with builder FormGroups', () => { + const c = fb.group({foo: fb.group({baz: 'bar'})}); + { + type ControlsType = {foo: FormGroup<{baz: FormControl}>}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with builder FormArrays', () => { + const c = fb.group({foo: fb.array(['bar'])}); + { + type ControlsType = {foo: FormArray>}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + }); + }); + + describe('should build FormArrays', () => { + it('from arrays with plain values', () => { + const c = fb.array(['foo']); + { + type ControlsType = Array>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from arrays with FormControlStates', () => { + const c = fb.array([{value: 'foo', disabled: false}]); + { + type ControlsType = Array>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from arrays with ControlConfigs', () => { + const c = fb.array([['foo']]); + { + type ControlsType = Array>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + describe('from arrays with FormControls', () => { + it('nullably', () => { + const c = fb.array([new FormControl('foo')]); + { + type ControlsType = Array>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('non-nullably', () => { + const c = fb.array([new FormControl('foo', {initialValueIsDefault: true})]); + { + type ControlsType = Array>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + }); + + it('from arrays with direct FormArrays', () => { + const c = fb.array([new FormArray([new FormControl('foo')])]); + { + type ControlsType = Array>>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from arrays with builder FormArrays', () => { + const c = fb.array([fb.array(['foo'])]); + { + type ControlsType = Array>>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from arrays with builder FormGroups', () => { + const c = fb.array([fb.group({bar: 'foo'})]); + { + type ControlsType = Array}>>; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + }); + + it('should work with a complex, deeply nested case', () => { + // Mix a variety of different construction methods and argument types. + const myParty = fb.group({ + venue: fb.group({ + location: 'San Francisco', + date: fb.group({ + year: {value: 2022, disabled: false}, + month: fb.control('December', {}), + day: fb.control(new FormControl(14)), + }) + }), + dinnerOptions: fb.array([fb.group({ + food: fb.group({ + entree: ['Souffle', Validators.required], + dessert: 'also Souffle', + }), + price: fb.group({ + amount: new FormControl(50, {initialValueIsDefault: true}), + currency: 'USD', + }) + })]) + }); + { + type ControlType = { + venue: FormGroup<{ + location: FormControl, + date: FormGroup<{ + year: FormControl, + month: FormControl, + day: FormControl, + }>, + }>, + dinnerOptions: FormArray, + dessert: FormControl, + }>, + price: FormGroup<{ + amount: FormControl, + currency: FormControl, + }>, + }>>, + }; + let t: ControlType = myParty.controls; + let t1 = myParty.controls; + t1 = null as unknown as ControlType; + } + }); + }); +}); + +describe('Untyped Class', () => { + describe('UntypedFormControl', () => { + it('should function like a FormControl with the default type', () => { + const ufc = new UntypedFormControl('foo'); + expect(ufc.value).toEqual('foo'); + }); + + it('should default to null with no argument', () => { + const ufc = new UntypedFormControl(); + expect(ufc.value).toEqual(null); + }); + + it('is assignable with the typed version in both directions', () => { + const fc: FormControl = new UntypedFormControl(''); + const ufc: UntypedFormControl = new FormControl(''); + }); + + it('is an escape hatch from a strongly-typed FormControl', () => { + let fc = new FormControl(42); + const ufc = new UntypedFormControl('foo'); + fc = ufc; + }); + }); + + describe('UntypedFormGroup', () => { + it('should function like a FormGroup with the default type', () => { + const ufc = new UntypedFormGroup({foo: new FormControl('bar')}); + expect(ufc.value).toEqual({foo: 'bar'}); + const fc = ufc.get('foo'); + }); + + it('should allow dotted access to properties', () => { + const ufc = new UntypedFormGroup({foo: new FormControl('bar')}); + expect(ufc.value.foo).toEqual('bar'); + }); + + it('should allow access to AbstractControl methods', () => { + const ufc = new UntypedFormGroup({foo: new FormControl('bar')}); + expect(ufc.validator).toBe(null); + }); + + it('is assignable with the typed version in both directions', () => { + const fc: FormGroup<{foo: FormControl}> = + new UntypedFormGroup({foo: new UntypedFormControl('')}); + const ufc: UntypedFormGroup = new FormGroup({foo: new FormControl('')}); + }); + + it('is assignable to FormGroup', () => { + let fg: FormGroup<{foo: FormControl}>; + const ufg = new UntypedFormGroup({foo: new FormControl('bar')}); + fg = ufg; + }); + + it('is an escape hatch from a strongly-typed FormGroup', () => { + let fg = new FormGroup({foo: new FormControl(42)}); + const ufg = new UntypedFormGroup({foo: new FormControl('bar')}); + fg = ufg; + }); + }); + + describe('UntypedFormArray', () => { + it('should function like a FormArray with the default type', () => { + const ufc = new UntypedFormArray([new FormControl('foo')]); + expect(ufc.value).toEqual(['foo']); + ufc.valueChanges.subscribe(v => v); + }); + + it('is assignable with the typed version in both directions', () => { + const ufa: UntypedFormArray = new FormArray([new FormControl('')]); + const fa: FormArray> = + new UntypedFormArray([new UntypedFormControl('')]); + }); + }); + + describe('UntypedFormBuilder', () => { + let fb: FormBuilder = new FormBuilder(); + let ufb: UntypedFormBuilder = new UntypedFormBuilder(); + + function typedFn(fb: FormBuilder): void {} + function untypedFn(fb: UntypedFormBuilder): void {} + + beforeEach(() => { + ufb = new UntypedFormBuilder(); + }); + + it('should build untyped FormControls', () => { + const ufc = ufb.control(42); + expect(ufc.value).toEqual(42); + }); + + it('should build untyped FormGroups', () => { + const ufc = ufb.group({ + 'foo': 1, + 'bar': 2, + }); + expect(ufc.value.foo).toEqual(1); + }); + + it('can be provided where a FormBuilder is expected and vice versa', () => { + typedFn(ufb); + untypedFn(fb); + }); + }); +});