mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(forms): Add FormBuilder.nonNullable. (#45852)
With typed forms, all `FormControl`s are nullable by default, because they can be reset to `null`. This behavior is possible to change by passing the option `initialValueIsDefault: true`. However, in a large form, this is extremely cumbersome, as the option must be repeated over and over. Additionally, it is not possible to take full advantage of `FormBuilder`, since `FormBuilder.group` and `FormBuilder.array` will produce nullable controls.
This PR introduces a new accessor `FormBuilder.nonNullable`, which produces *non-nullable* controls. Specifically, any call to `.control` will produce controls with `{initialValueIsDefault: true}`, and calls to `.array` or `.group` that implicitly build inner controls will have the same effect.
```ts
let nfb = new FormBuilder().nonNullable;
let name = nfb.group({who: 'Alex'}); // FormGroup<{who: FormControl<string>}>
name.reset();
console.log(name); // {who: 'Alex'}
```
PR Close #45852
This commit is contained in:
@@ -1076,6 +1076,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAbstractControlOptions"
|
||||
},
|
||||
{
|
||||
"name": "isAnimationProp"
|
||||
},
|
||||
|
||||
@@ -38,26 +38,28 @@ function isFormControlOptions(options: FormControlOptions|{[key: string]: any}|n
|
||||
*/
|
||||
export type ControlConfig<T> = [T|FormControlState<T>, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?];
|
||||
|
||||
// Disable clang-format to produce clearer formatting for these multiline types.
|
||||
|
||||
// Disable clang-format to produce clearer formatting for this multiline type.
|
||||
// clang-format off
|
||||
|
||||
/**
|
||||
* FormBuilder accepts values in various container shapes, as well as raw values.
|
||||
* Element returns the appropriate corresponding model class.
|
||||
* Element returns the appropriate corresponding model class, given the container T.
|
||||
* The flag N, if not never, makes the resulting `FormControl` have N in its type.
|
||||
*/
|
||||
export type ɵElement<T> =
|
||||
export type ɵElement<T, N extends null> =
|
||||
T extends FormControl<infer U> ? FormControl<U> :
|
||||
T extends FormGroup<infer U> ? FormGroup<U> :
|
||||
T extends FormArray<infer U> ? FormArray<U> :
|
||||
T extends AbstractControl<infer U> ? AbstractControl<U> :
|
||||
T extends FormControlState<infer U> ? FormControl<U|null> :
|
||||
T extends ControlConfig<infer U> ? FormControl<U|null> :
|
||||
T extends FormControlState<infer U> ? FormControl<U|N> :
|
||||
T extends ControlConfig<infer U> ? FormControl<U|N> :
|
||||
// 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<infer U|ValidatorFn|ValidatorFn[]|AsyncValidatorFn|AsyncValidatorFn[]> ? FormControl<U|null> :
|
||||
// Fallthough case: T is not a container type; use is directly as a value.
|
||||
FormControl<T|null>;
|
||||
T extends Array<infer U|ValidatorFn|ValidatorFn[]|AsyncValidatorFn|AsyncValidatorFn[]> ? FormControl<U|N> :
|
||||
// Fallthough case: T is not a container type; use it directly as a value.
|
||||
FormControl<T|N>;
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -75,6 +77,56 @@ export type ɵElement<T> =
|
||||
*/
|
||||
@Injectable({providedIn: ReactiveFormsModule})
|
||||
export class FormBuilder {
|
||||
private useNonNullable: boolean = false;
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Returns a FormBuilder in which automatically constructed @see FormControl} elements
|
||||
* have `{initialValueIsDefault: true}` and are non-nullable.
|
||||
*
|
||||
* **Constructing non-nullable controls**
|
||||
*
|
||||
* When constructing a control, it will be non-nullable, and will reset to its initial value.
|
||||
*
|
||||
* ```ts
|
||||
* let nnfb = new FormBuilder().nonNullable;
|
||||
* let name = nnfb.control('Alex'); // FormControl<string>
|
||||
* name.reset();
|
||||
* console.log(name); // 'Alex'
|
||||
* ```
|
||||
*
|
||||
* **Constructing non-nullable groups or arrays**
|
||||
*
|
||||
* When constructing a group or array, all automatically created inner controls will be
|
||||
* non-nullable, and will reset to their initial values.
|
||||
*
|
||||
* ```ts
|
||||
* let nnfb = new FormBuilder().nonNullable;
|
||||
* let name = nnfb.group({who: 'Alex'}); // FormGroup<{who: FormControl<string>}>
|
||||
* name.reset();
|
||||
* console.log(name); // {who: 'Alex'}
|
||||
* ```
|
||||
* **Constructing *nullable* fields on groups or arrays**
|
||||
*
|
||||
* It is still possible to have a nullable field. In particular, any `FormControl` which is
|
||||
* *already* constructed will not be altered. For example:
|
||||
*
|
||||
* ```ts
|
||||
* let nnfb = new FormBuilder().nonNullable;
|
||||
* // FormGroup<{who: FormControl<string|null>}>
|
||||
* let name = nnfb.group({who: new FormControl('Alex')});
|
||||
* name.reset(); console.log(name); // {who: null}
|
||||
* ```
|
||||
*
|
||||
* Because the inner control is constructed explicitly by the caller, the builder has
|
||||
* no control over how it is created, and cannot exclude the `null`.
|
||||
*/
|
||||
get nonNullable(): NonNullableFormBuilder {
|
||||
const nnfb = new FormBuilder();
|
||||
nnfb.useNonNullable = true;
|
||||
return nnfb as NonNullableFormBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Construct a new `FormGroup` instance. Accepts a single generic argument, which is an object
|
||||
@@ -93,7 +145,7 @@ export class FormBuilder {
|
||||
group<T extends {}>(
|
||||
controls: T,
|
||||
options?: AbstractControlOptions|null,
|
||||
): FormGroup<{[K in keyof T]: ɵElement<T[K]>}>;
|
||||
): FormGroup<{[K in keyof T]: ɵElement<T[K], null>}>;
|
||||
|
||||
/**
|
||||
* @description
|
||||
@@ -189,7 +241,19 @@ export class FormBuilder {
|
||||
formState: T|FormControlState<T>,
|
||||
validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl {
|
||||
return new FormControl(formState, validatorOrOpts, asyncValidator);
|
||||
let newOptions: FormControlOptions = {};
|
||||
if (!this.useNonNullable) {
|
||||
return new FormControl(formState, validatorOrOpts, asyncValidator);
|
||||
}
|
||||
if (isAbstractControlOptions(validatorOrOpts)) {
|
||||
// If the second argument is options, then they are copied.
|
||||
newOptions = validatorOrOpts;
|
||||
} else {
|
||||
// If the other arguments are validators, they are copied into an options object.
|
||||
newOptions.validators = validatorOrOpts;
|
||||
newOptions.asyncValidators = asyncValidator;
|
||||
}
|
||||
return new FormControl<T>(formState, {...newOptions, initialValueIsDefault: true});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +272,7 @@ export class FormBuilder {
|
||||
*/
|
||||
array<T>(
|
||||
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T>> {
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T, null>> {
|
||||
const createdControls = controls.map(c => this._createControl(c));
|
||||
// Cast to `any` because the inferred types are not as specific as Element.
|
||||
return new FormArray(createdControls, validatorOrOpts, asyncValidator) as any;
|
||||
@@ -244,13 +308,50 @@ export class FormBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* `NonNullableFormBuilder` is similar to {@see FormBuilder}, but automatically constructed
|
||||
* {@see FormControl} elements have `{initialValueIsDefault: true}` and are non-nullable.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface NonNullableFormBuilder {
|
||||
/**
|
||||
* Similar to {@see FormBuilder#group}, except any implicitly constructed `FormControl`
|
||||
* will be non-nullable (i.e. it will have `initialValueIsDefault` set to true). Note
|
||||
* that already-constructed controls will not be altered.
|
||||
*/
|
||||
group<T extends {}>(
|
||||
controls: T,
|
||||
options?: AbstractControlOptions|null,
|
||||
): FormGroup<{[K in keyof T]: ɵElement<T[K], never>}>;
|
||||
|
||||
/**
|
||||
* Similar to {@see FormBuilder#array}, except any implicitly constructed `FormControl`
|
||||
* will be non-nullable (i.e. it will have `initialValueIsDefault` set to true). Note
|
||||
* that already-constructed controls will not be altered.
|
||||
*/
|
||||
array<T>(
|
||||
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T, never>>;
|
||||
|
||||
/**
|
||||
* Similar to {@see FormBuilder#control}, except this overridden version of `control` forces
|
||||
* `initialValueIsDefault` to be `true`, resulting in the control always being non-nullable.
|
||||
*/
|
||||
control<T>(
|
||||
formState: T|FormControlState<T>,
|
||||
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
||||
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* UntypedFormBuilder is the same as @see FormBuilder, but it provides untyped controls.
|
||||
*/
|
||||
@Injectable({providedIn: ReactiveFormsModule})
|
||||
export class UntypedFormBuilder extends FormBuilder {
|
||||
/**
|
||||
* @see FormBuilder#group
|
||||
* Like {@see FormBuilder#group}, except the resulting group is untyped.
|
||||
*/
|
||||
override group(
|
||||
controlsConfig: {[key: string]: any},
|
||||
@@ -273,7 +374,7 @@ export class UntypedFormBuilder extends FormBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FormBuilder#control
|
||||
* Like {@see FormBuilder#control}, except the resulting control is untyped.
|
||||
*/
|
||||
override control(
|
||||
formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
|
||||
@@ -282,7 +383,7 @@ export class UntypedFormBuilder extends FormBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see FormBuilder#array
|
||||
* Like {@see FormBuilder#array}, except the resulting array is untyped.
|
||||
*/
|
||||
override array(
|
||||
controlsConfig: any[],
|
||||
|
||||
@@ -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, ɵElement} from './form_builder';
|
||||
export {FormBuilder, NonNullableFormBuilder, 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';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// 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 {FormBuilder, NonNullableFormBuilder, UntypedFormBuilder} from '../src/form_builder';
|
||||
import {AbstractControl, FormArray, FormControl, FormGroup, UntypedFormArray, UntypedFormControl, UntypedFormGroup, Validators} from '../src/forms';
|
||||
import {FormRecord} from '../src/model/form_group';
|
||||
|
||||
@@ -1270,6 +1270,191 @@ describe('Typed Class', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('NonNullFormBuilder', () => {
|
||||
let fb: NonNullableFormBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
fb = new FormBuilder().nonNullable;
|
||||
});
|
||||
|
||||
describe('should build FormControls', () => {
|
||||
it('non-nullably from values', () => {
|
||||
const c = fb.control('foo');
|
||||
{
|
||||
type RawValueType = string;
|
||||
let t: RawValueType = c.getRawValue();
|
||||
let t1 = c.getRawValue();
|
||||
t1 = null as unknown as RawValueType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).not.toBeNull;
|
||||
});
|
||||
});
|
||||
|
||||
describe('should build FormGroups', () => {
|
||||
it('from objects with plain values', () => {
|
||||
const c = fb.group({foo: 'bar'});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('from objects with FormControlState', () => {
|
||||
const c = fb.group({foo: {value: 'bar', disabled: false}});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('from objects with ControlConfigs', () => {
|
||||
const c = fb.group({foo: ['bar']});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('from objects with ControlConfigs and validators', () => {
|
||||
const c = fb.group({foo: ['bar', Validators.required]});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('from objects with ControlConfigs and validator lists', () => {
|
||||
const c = fb.group({foo: ['bar', [Validators.required, Validators.email]]});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('from objects with ControlConfigs and explicit types', () => {
|
||||
const c: FormGroup<{foo: FormControl<string>}> =
|
||||
fb.group({foo: ['bar', [Validators.required, Validators.email]]});
|
||||
{
|
||||
type ControlsType = {foo: FormControl<string>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
describe('from objects with FormControls', () => {
|
||||
it('from objects with builder FormGroups', () => {
|
||||
const c = fb.group({foo: fb.group({baz: 'bar'})});
|
||||
{
|
||||
type ControlsType = {foo: FormGroup<{baz: FormControl<string>}>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: {baz: 'bar'}});
|
||||
});
|
||||
|
||||
it('from objects with builder FormArrays', () => {
|
||||
const c = fb.group({foo: fb.array(['bar'])});
|
||||
{
|
||||
type ControlsType = {foo: FormArray<FormControl<string>>};
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual({foo: ['bar']});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should build FormArrays', () => {
|
||||
it('from arrays with plain values', () => {
|
||||
const c = fb.array(['foo']);
|
||||
{
|
||||
type ControlsType = Array<FormControl<string>>;
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual(['foo']);
|
||||
});
|
||||
|
||||
it('from arrays with FormControlStates', () => {
|
||||
const c = fb.array([{value: 'foo', disabled: false}]);
|
||||
{
|
||||
type ControlsType = Array<FormControl<string>>;
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual(['foo']);
|
||||
});
|
||||
|
||||
it('from arrays with ControlConfigs', () => {
|
||||
const c = fb.array([['foo']]);
|
||||
{
|
||||
type ControlsType = Array<FormControl<string>>;
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual(['foo']);
|
||||
});
|
||||
|
||||
it('from arrays with builder FormArrays', () => {
|
||||
const c = fb.array([fb.array(['foo'])]);
|
||||
{
|
||||
type ControlsType = Array<FormArray<FormControl<string>>>;
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual([['foo']]);
|
||||
});
|
||||
|
||||
it('from arrays with builder FormGroups', () => {
|
||||
const c = fb.array([fb.group({bar: 'foo'})]);
|
||||
{
|
||||
type ControlsType = Array<FormGroup<{bar: FormControl<string>}>>;
|
||||
let t: ControlsType = c.controls;
|
||||
let t1 = c.controls;
|
||||
t1 = null as unknown as ControlsType;
|
||||
}
|
||||
c.reset();
|
||||
expect(c.value).toEqual([{bar: 'foo'}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Untyped Class', () => {
|
||||
|
||||
Reference in New Issue
Block a user