Skip to content

Commit

Permalink
feat(forms): add FormBuilder.record() method (#46485)
Browse files Browse the repository at this point in the history
The new `FormRecord` entity introduced in Angular v14 does not have its builder method.
This commit adds it, allowing to write:

```
const fb = new FormBuilder();
fb.record({ a: 'one' });
```

This works for both the `FormBuilder` and the `NonNullableFormBuilder`

PR Close #46485
  • Loading branch information
cexbrayat authored and thePunderWoman committed Jul 15, 2022
1 parent 089efa1 commit 426af91
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 5 deletions.
6 changes: 6 additions & 0 deletions aio/content/guide/typed-forms.md
Expand Up @@ -171,6 +171,12 @@ Any control of type `string|null` can be added to this `FormRecord`.

If you need a `FormGroup` that is both dynamic (open-ended) and heterogenous (the controls are different types), no improved type safety is possible, and you should use `UntypedFormGroup`.

A `FormRecord` can also be built with the `FormBuilder`:

```ts
const addresses = fb.record({'Andrew': '2340 Folsom St'});
```

## `FormBuilder` and `NonNullableFormBuilder`

The `FormBuilder` class has been upgraded to support the new types as well, in the same manner as the above examples.
Expand Down
8 changes: 7 additions & 1 deletion goldens/public-api/forms/index.md
Expand Up @@ -302,6 +302,9 @@ export class FormBuilder {
[key: string]: any;
}): FormGroup;
get nonNullable(): NonNullableFormBuilder;
record<T>(controls: {
[key: string]: T;
}, options?: AbstractControlOptions | null): FormRecordElement<T, null>>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<FormBuilder, never>;
// (undocumented)
Expand Down Expand Up @@ -505,7 +508,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
}

// @public
export class FormRecord<TControl extends AbstractControlValue<TControl>, ɵRawValue<TControl>> = AbstractControl> extends FormGroup<{
export class FormRecord<TControl extends AbstractControl = AbstractControl> extends FormGroup<{
[key: string]: TControl;
}> {
}
Expand Down Expand Up @@ -723,6 +726,9 @@ export abstract class NonNullableFormBuilder {
abstract group<T extends {}>(controls: T, options?: AbstractControlOptions | null): FormGroup<{
[K in keyof T]: ɵElement<T[K], never>;
}>;
abstract record<T>(controls: {
[key: string]: T;
}, options?: AbstractControlOptions | null): FormRecordElement<T, never>>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NonNullableFormBuilder, never>;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Expand Up @@ -41,7 +41,7 @@
"forms": {
"uncompressed": {
"runtime": 1063,
"main": 157479,
"main": 158224,
"polyfills": 33804
}
},
Expand Down
Expand Up @@ -194,6 +194,9 @@
{
"name": "FormGroupName"
},
{
"name": "FormRecord"
},
{
"name": "FormsExampleModule"
},
Expand Down
38 changes: 37 additions & 1 deletion packages/forms/src/form_builder.ts
Expand Up @@ -13,7 +13,7 @@ import {ReactiveFormsModule} from './form_providers';
import {AbstractControl, AbstractControlOptions, FormHooks} from './model/abstract_model';
import {FormArray, UntypedFormArray} from './model/form_array';
import {FormControl, FormControlOptions, FormControlState, UntypedFormControl} from './model/form_control';
import {FormGroup, UntypedFormGroup} from './model/form_group';
import {FormGroup, FormRecord, UntypedFormGroup} from './model/form_group';

function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]: any}|null|
undefined): options is AbstractControlOptions {
Expand Down Expand Up @@ -53,6 +53,10 @@ export type ɵElement<T, N extends null> =
[T] extends [FormGroup<infer U>] ? FormGroup<U> :
// Optional FormGroup containers.
[T] extends [FormGroup<infer U>|undefined] ? FormGroup<U> :
// FormRecord containers.
[T] extends [FormRecord<infer U>] ? FormRecord<U> :
// Optional FormRecord containers.
[T] extends [FormRecord<infer U>|undefined] ? FormRecord<U> :
// FormArray containers.
[T] extends [FormArray<infer U>] ? FormArray<U> :
// Optional FormArray containers.
Expand Down Expand Up @@ -202,6 +206,28 @@ export class FormBuilder {
return new FormGroup(reducedControls, newOptions);
}

/**
* @description
* Construct a new `FormRecord` instance. Accepts a single generic argument, which is an object
* containing all the keys and corresponding inner control types.
*
* @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 `FormRecord`. 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').
*/
record<T>(controls: {[key: string]: T}, options: AbstractControlOptions|null = null):
FormRecord<ɵElement<T, null>> {
const reducedControls = this._reduceControls(controls);
// Cast to `any` because the inferred types are not as specific as Element.
return new FormRecord(reducedControls, options) as any;
}

/** @deprecated Use `nonNullable` instead. */
control<T>(formState: T|FormControlState<T>, opts: FormControlOptions&{
initialValueIsDefault: true
Expand Down Expand Up @@ -341,6 +367,16 @@ export abstract class NonNullableFormBuilder {
options?: AbstractControlOptions|null,
): FormGroup<{[K in keyof T]: ɵElement<T[K], never>}>;

/**
* Similar to `FormBuilder#record`, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `nonNullable` set to true). Note
* that already-constructed controls will not be altered.
*/
abstract record<T>(
controls: {[key: string]: T},
options?: AbstractControlOptions|null,
): FormRecord<ɵElement<T, never>>;

/**
* Similar to `FormBuilder#array`, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `nonNullable` set to true). Note
Expand Down
3 changes: 1 addition & 2 deletions packages/forms/src/model/form_group.ts
Expand Up @@ -625,8 +625,7 @@ export const isFormGroup = (control: unknown): control is FormGroup => control i
*
* @publicApi
*/
export class FormRecord<TControl extends AbstractControl<ɵValue<TControl>, ɵRawValue<TControl>> =
AbstractControl> extends
export class FormRecord<TControl extends AbstractControl = AbstractControl> extends
FormGroup<{[key: string]: TControl}> {}

export interface FormRecord<TControl> {
Expand Down
26 changes: 26 additions & 0 deletions packages/forms/test/form_builder_spec.ts
Expand Up @@ -68,6 +68,32 @@ describe('Form Builder', () => {
expect(g.controls['login'].value).toEqual('some value');
});

describe('should create control records', () => {
it('from simple values', () => {
const a = b.record({a: 'one', b: 'two'});
expect(a.value).toEqual({a: 'one', b: 'two'});
});

it('from boxed values', () => {
const a = b.record({a: 'one', b: {value: 'two', disabled: true}});
expect(a.value).toEqual({a: 'one'});
a.get('b')?.enable();
expect(a.value).toEqual({a: 'one', b: 'two'});
});

it('from an array', () => {
const a = b.record({a: ['one']});
expect(a.value).toEqual({a: 'one'});
});

it('from controls whose form state is a primitive value', () => {
const record = b.record({'login': b.control('some value', syncValidator, asyncValidator)});

expect(record.controls['login'].value).toEqual('some value');
expect(record.controls['login'].validator).toBe(syncValidator);
expect(record.controls['login'].asyncValidator).toBe(asyncValidator);
});
});
it('should create homogenous control arrays', () => {
const a = b.array(['one', 'two', 'three']);
expect(a.value).toEqual(['one', 'two', 'three']);
Expand Down
135 changes: 135 additions & 0 deletions packages/forms/test/typed_integration_spec.ts
Expand Up @@ -1132,6 +1132,16 @@ describe('Typed Class', () => {
}
});

it('from objects with builder FormRecords', () => {
const c = fb.group({foo: fb.record({baz: 'bar'})});
{
type ControlsType = {foo: FormRecord<FormControl<string|null>>};
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'])});
{
Expand All @@ -1144,6 +1154,121 @@ describe('Typed Class', () => {
});
});

describe('should build FormRecords', () => {
it('from objects with plain values', () => {
const c = fb.record({foo: 'bar'});
{
type ControlsType = {[key: string]: FormControl<string|null>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with FormControlState', () => {
const c = fb.record({foo: {value: 'bar', disabled: false}});
{
type ControlsType = {[key: string]: FormControl<string|null>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with ControlConfigs', () => {
const c = fb.record({foo: ['bar']});
{
type ControlsType = {[key: string]: FormControl<string|null>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with ControlConfigs and validators', () => {
const c = fb.record({foo: ['bar', Validators.required]});
{
type ControlsType = {[key: string]: FormControl<string|null>};
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.record({foo: ['bar', [Validators.required, Validators.email]]});
{
type ControlsType = {[key: string]: FormControl<string|null>};
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: FormRecord<FormControl<string|null>> =
fb.record({foo: ['bar', [Validators.required, Validators.email]]});
{
type ControlsType = {[key: string]: FormControl<string|null>};
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.record({foo: new FormControl('bar')});
{
type ControlsType = {[key: string]: FormControl<string|null>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('non-nullably', () => {
const c = fb.record({foo: new FormControl('bar', {nonNullable: true})});
{
type ControlsType = {[key: string]: FormControl<string>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with builder FormGroups', () => {
const c = fb.record({foo: fb.group({baz: 'bar'})});
{
type ControlsType = {[key: string]: FormGroup<{baz: FormControl<string|null>}>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with builder FormRecords', () => {
const c = fb.record({foo: fb.record({baz: 'bar'})});
{
type ControlsType = {[key: string]: FormRecord<FormControl<string|null>>};
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});

it('from objects with builder FormArrays', () => {
const c = fb.record({foo: fb.array(['bar'])});
{
type ControlsType = {[key: string]: FormArray<FormControl<string|null>>};
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']);
Expand Down Expand Up @@ -1226,6 +1351,16 @@ describe('Typed Class', () => {
t1 = null as unknown as ControlsType;
}
});

it('from arrays with builder FormRecords', () => {
const c = fb.array([fb.record({bar: 'foo'})]);
{
type ControlsType = Array<FormRecord<FormControl<string|null>>>;
let t: ControlsType = c.controls;
let t1 = c.controls;
t1 = null as unknown as ControlsType;
}
});
});

it('should work with a complex, deeply nested case', () => {
Expand Down

0 comments on commit 426af91

Please sign in to comment.