diff --git a/goldens/public-api/forms/index.md b/goldens/public-api/forms/index.md index 07ba96cb2ea..7cb10d9f90f 100644 --- a/goldens/public-api/forms/index.md +++ b/goldens/public-api/forms/index.md @@ -276,18 +276,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy // @public export class FormBuilder { - // (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>; + array(controls: Array, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<ɵElement>; // (undocumented) control(formState: T | FormControlState, opts: FormControlOptions & { initialValueIsDefault: true; @@ -295,10 +284,8 @@ export class FormBuilder { // (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; + group(controls: T, options?: AbstractControlOptions | null): FormGroup<{ + [K in keyof T]: ɵElement; }>; // @deprecated group(controls: { diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index 165869645e6..277cb47976d 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -15,11 +15,19 @@ 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 { - return (options).asyncValidators !== undefined || - (options).validators !== undefined || - (options).updateOn !== undefined; +function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]: any}|null| + undefined): options is AbstractControlOptions { + return !!options && + ((options as AbstractControlOptions).asyncValidators !== undefined || + (options as AbstractControlOptions).validators !== undefined || + (options as AbstractControlOptions).updateOn !== undefined); +} + +function isFormControlOptions(options: FormControlOptions|{[key: string]: any}|null| + undefined): options is FormControlOptions { + return !!options && + (isAbstractControlOptions(options) || + (options as FormControlOptions).initialValueIsDefault !== undefined); } /** @@ -30,15 +38,25 @@ function isAbstractControlOptions(options: AbstractControlOptions| */ export type ControlConfig = [T|FormControlState, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?]; -// Disable clang-format to produce clearer formatting for this multiline type. +// Disable clang-format to produce clearer formatting for these multiline types. // clang-format off -export type ɵGroupElement = + +/** + * FormBuilder accepts values in various container shapes, as well as raw values. + * Element returns the appropriate corresponding model class. + */ +export type ɵElement = T extends FormControl ? FormControl : T extends FormGroup ? FormGroup : T extends FormArray ? FormArray : T extends AbstractControl ? AbstractControl : T extends FormControlState ? FormControl : T extends ControlConfig ? FormControl : + // ControlConfig can be too much for the compiler to infer in the wrapped case. This is + // not surprising, since it's practically death-by-polymorphism (e.g. the optional validators + // members that might be arrays). Watch for ControlConfigs that might fall through. + T extends Array ? FormControl : + // Fallthough case: T is not a container type; use is directly as a value. FormControl; // clang-format on @@ -57,13 +75,10 @@ export type ɵGroupElement = */ @Injectable({providedIn: ReactiveFormsModule}) export class FormBuilder { - group| ControlConfig| FormControl| FormGroup| - FormArray| AbstractControl| T[K] - }>( + group( controls: T, options?: AbstractControlOptions|null, - ): FormGroup<{[K in keyof T]: ɵGroupElement}>; + ): FormGroup<{[K in keyof T]: ɵElement}>; /** * @description @@ -124,12 +139,14 @@ export class FormBuilder { updateOn = options.updateOn != null ? options.updateOn : undefined; } else { // `options` are legacy form group options - validators = options['validator'] != null ? options['validator'] : null; - asyncValidators = options['asyncValidator'] != null ? options['asyncValidator'] : null; + validators = (options as any)['validator'] != null ? (options as any)['validator'] : null; + asyncValidators = + (options as any)['asyncValidator'] != null ? (options as any)['asyncValidator'] : null; } } - return new FormGroup(reducedControls, {asyncValidators, updateOn, validators}); + // Cast to `any` because the inferred types are not as specific as Element. + return new FormGroup(reducedControls, {asyncValidators, updateOn, validators}) as any; } control(formState: T|FormControlState, opts: FormControlOptions&{ @@ -169,36 +186,10 @@ export class FormBuilder { control( formState: T|FormControlState, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| - null): FormControl|FormControl { + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): 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. @@ -212,11 +203,12 @@ export class FormBuilder { * * @param asyncValidator A single async validator or array of async validator functions. */ - array( - controls: any[], validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray { + array( + controls: Array, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement> { const createdControls = controls.map(c => this._createControl(c)); - return new FormArray(createdControls, validatorOrOpts, asyncValidator); + // Cast to `any` because the inferred types are not as specific as Element. + return new FormArray(createdControls, validatorOrOpts, asyncValidator) as any; } /** @internal */ @@ -261,13 +253,16 @@ export class UntypedFormBuilder extends FormBuilder { controlsConfig: {[key: string]: any}, options?: AbstractControlOptions|null, ): UntypedFormGroup; + /** - * @deprecated + * @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming. + * Use the `FormBuilder#group` overload with `AbstractControlOptions` instead. */ override group( controlsConfig: {[key: string]: any}, options: {[key: string]: any}, ): UntypedFormGroup; + override group( controlsConfig: {[key: string]: any}, options: AbstractControlOptions|{[key: string]: any}|null = null): UntypedFormGroup { diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 6a50a1236c0..e13994e17a0 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -41,7 +41,7 @@ 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, ɵGroupElement} from './form_builder'; +export {FormBuilder, UntypedFormBuilder, ɵElement} 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'; diff --git a/packages/forms/test/typed_integration_spec.ts b/packages/forms/test/typed_integration_spec.ts index 0de68998772..1b11402ae0c 100644 --- a/packages/forms/test/typed_integration_spec.ts +++ b/packages/forms/test/typed_integration_spec.ts @@ -1052,6 +1052,37 @@ describe('Typed Class', () => { } }); + it('from objects with ControlConfigs and validators', () => { + const c = fb.group({foo: ['bar', Validators.required]}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with ControlConfigs and validator lists', () => { + const c = fb.group({foo: ['bar', [Validators.required, Validators.email]]}); + { + type ControlsType = {foo: FormControl}; + let t: ControlsType = c.controls; + let t1 = c.controls; + t1 = null as unknown as ControlsType; + } + }); + + it('from objects with ControlConfigs and explicit types', () => { + const c: FormGroup<{foo: FormControl}> = + fb.group({foo: ['bar', [Validators.required, Validators.email]]}); + { + 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')}); @@ -1233,6 +1264,7 @@ describe('Typed Class', () => { }>>, }; let t: ControlType = myParty.controls; + let d = myParty.controls.dinnerOptions; let t1 = myParty.controls; t1 = null as unknown as ControlType; }