Skip to content

Commit

Permalink
feat(forms): Add a FormRecord type. (#45607)
Browse files Browse the repository at this point in the history
As part of the typed forms RFC, we proposed the creation of a new FormRecord type, to support dynamic groups with homogenous values. This PR introduces FormRecord, as a subclass of FormGroup.

PR Close #45607
  • Loading branch information
dylhunn committed Apr 14, 2022
1 parent f8a1ea0 commit e0a2248
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
42 changes: 42 additions & 0 deletions goldens/public-api/forms/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,48 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
static ɵfac: i0.ɵɵFactoryDeclaration<FormGroupName, [{ optional: true; host: true; skipSelf: true; }, { optional: true; self: true; }, { optional: true; self: true; }]>;
}

// @public (undocumented)
export class FormRecord<TControl extends AbstractControlValue<TControl>, ɵRawValue<TControl>> = AbstractControl> extends FormGroup<{
[key: string]: TControl;
}> {
}

// @public
export interface FormRecord<TControl> {
addControl(name: string, control: TControl, options?: {
emitEvent?: boolean;
}): void;
contains(controlName: string): boolean;
getRawValue(): {
[key: string]: ɵRawValue<TControl>;
};
patchValue(value: {
[key: string]: ɵValue<TControl>;
}, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
registerControl(name: string, control: TControl): TControl;
removeControl(name: string, options?: {
emitEvent?: boolean;
}): void;
reset(value?: {
[key: string]: ɵValue<TControl>;
}, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
setControl(name: string, control: TControl, options?: {
emitEvent?: boolean;
}): void;
setValue(value: {
[key: string]: ɵValue<TControl>;
}, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
}): void;
}

// @public
export class FormsModule {
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/src/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ 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 {FormGroup, FormRecord, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
export {VERSION} from './version';

Expand Down
94 changes: 94 additions & 0 deletions packages/forms/src/model/form_group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,97 @@ export type UntypedFormGroup = FormGroup<any>;
export const UntypedFormGroup: UntypedFormGroupCtor = FormGroup;

export const isFormGroup = (control: unknown): control is FormGroup => control instanceof FormGroup;

export class FormRecord<TControl extends AbstractControl<ɵValue<TControl>, ɵRawValue<TControl>> =
AbstractControl> extends
FormGroup<{[key: string]: TControl}> {}

/**
* Tracks the value and validity state of a collection of `FormControl` instances, each of which has
* the same value type.
*
* `FormRecord` is very similar to {@see FormGroup}, except it enforces that all controls in the group have the same type,
* and can be used with an open-ended, dynamically changing set of controls.
*
* @publicApi
*/
export interface FormRecord<TControl> {
/**
* Registers a control with the records's list of controls.
*
* {@see FormGroup#registerControl}
*/
registerControl(name: string, control: TControl): TControl;

/**
* Add a control to this group.
*
* {@see FormGroup#addControl}
*/
addControl(name: string, control: TControl, options?: {emitEvent?: boolean}): void;

/**
* Remove a control from this group.
*
* {@see FormGroup#removeControl}
*/
removeControl(name: string, options?: {emitEvent?: boolean}): void;

/**
* Replace an existing control.
*
* {@see FormGroup#setControl}
*/
setControl(name: string, control: TControl, options?: {emitEvent?: boolean}): void;

/**
* Check whether there is an enabled control with the given name in the group.
*
* {@see FormGroup#contains}
*/
contains(controlName: string): boolean;

/**
* Sets the value of the `FormRecord`. It accepts an object that matches
* the structure of the group, with control names as keys.
*
* {@see FormGroup#setValue}
*/
setValue(value: {[key: string]: ɵValue<TControl>}, options?: {
onlySelf?: boolean,
emitEvent?: boolean
}): void;

/**
* Patches the value of the `FormRecord`. It accepts an object with control
* names as keys, and does its best to match the values to the correct controls
* in the group.
*
* {@see FormGroup#patchValue}
*/
patchValue(value: {[key: string]: ɵValue<TControl>}, options?: {
onlySelf?: boolean,
emitEvent?: boolean
}): void;

/**
* Resets the `FormRecord`, marks all descendants `pristine` and `untouched` and sets
* the value of all descendants to null.
*
* {@see FormGroup#reset}
*/
reset(value?: {[key: string]: ɵValue<TControl>}, options?: {
onlySelf?: boolean,
emitEvent?: boolean
}): void;

/**
* The aggregate value of the `FormRecord`, including any disabled controls.
*
* {@see FormGroup#getRawValue}
*/
getRawValue(): {[key: string]: ɵRawValue<TControl>};
}

export const isFormRecord = (control: unknown): control is FormRecord =>
control instanceof FormRecord;
55 changes: 55 additions & 0 deletions packages/forms/test/typed_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import {FormBuilder, UntypedFormBuilder} from '../src/form_builder';
import {AbstractControl, FormArray, FormControl, FormGroup, UntypedFormArray, UntypedFormControl, UntypedFormGroup, Validators} from '../src/forms';
import {FormRecord} from '../src/model/form_group';

describe('Typed Class', () => {
describe('FormControl', () => {
Expand Down Expand Up @@ -636,6 +637,60 @@ describe('Typed Class', () => {
});
});

describe('FormRecord', () => {
it('supports inferred records', () => {
let c = new FormRecord({a: new FormControl(42, {initialValueIsDefault: true})});
{
type ValueType = Partial<{[key: string]: number}>;
let t: ValueType = c.value;
let t1 = c.value;
t1 = null as unknown as ValueType;
}
{
type RawValueType = {[key: string]: number};
let t: RawValueType = c.getRawValue();
let t1 = c.getRawValue();
t1 = null as unknown as RawValueType;
}
c.registerControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.addControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.setControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.removeControl('c');
c.removeControl('missing');
c.contains('c');
c.contains('foo');
c.setValue({a: 42});
c.patchValue({c: 42});
c.reset({c: 42, d: 0});
});

it('supports explicit records', () => {
let c = new FormRecord<FormControl<number>>(
{a: new FormControl(42, {initialValueIsDefault: true})});
{
type ValueType = Partial<{[key: string]: number}>;
let t: ValueType = c.value;
let t1 = c.value;
t1 = null as unknown as ValueType;
}
{
type RawValueType = {[key: string]: number};
let t: RawValueType = c.getRawValue();
let t1 = c.getRawValue();
t1 = null as unknown as RawValueType;
}
c.registerControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.addControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.setControl('c', new FormControl(42, {initialValueIsDefault: true}));
c.contains('c');
c.contains('foo');
c.setValue({a: 42, c: 0});
c.patchValue({c: 42});
c.reset({c: 42, d: 0});
c.removeControl('c');
});
});

describe('FormArray', () => {
it('supports inferred arrays', () => {
const c = new FormArray([new FormControl('', {initialValueIsDefault: true})]);
Expand Down

0 comments on commit e0a2248

Please sign in to comment.