Skip to content

Commit e0a2248

Browse files
committed
feat(forms): Add a FormRecord type. (angular#45607)
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 angular#45607
1 parent f8a1ea0 commit e0a2248

File tree

4 files changed

+192
-1
lines changed

4 files changed

+192
-1
lines changed

goldens/public-api/forms/index.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,48 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
506506
static ɵfac: i0.ɵɵFactoryDeclaration<FormGroupName, [{ optional: true; host: true; skipSelf: true; }, { optional: true; self: true; }, { optional: true; self: true; }]>;
507507
}
508508

509+
// @public (undocumented)
510+
export class FormRecord<TControl extends AbstractControlValue<TControl>, ɵRawValue<TControl>> = AbstractControl> extends FormGroup<{
511+
[key: string]: TControl;
512+
}> {
513+
}
514+
515+
// @public
516+
export interface FormRecord<TControl> {
517+
addControl(name: string, control: TControl, options?: {
518+
emitEvent?: boolean;
519+
}): void;
520+
contains(controlName: string): boolean;
521+
getRawValue(): {
522+
[key: string]: ɵRawValue<TControl>;
523+
};
524+
patchValue(value: {
525+
[key: string]: ɵValue<TControl>;
526+
}, options?: {
527+
onlySelf?: boolean;
528+
emitEvent?: boolean;
529+
}): void;
530+
registerControl(name: string, control: TControl): TControl;
531+
removeControl(name: string, options?: {
532+
emitEvent?: boolean;
533+
}): void;
534+
reset(value?: {
535+
[key: string]: ɵValue<TControl>;
536+
}, options?: {
537+
onlySelf?: boolean;
538+
emitEvent?: boolean;
539+
}): void;
540+
setControl(name: string, control: TControl, options?: {
541+
emitEvent?: boolean;
542+
}): void;
543+
setValue(value: {
544+
[key: string]: ɵValue<TControl>;
545+
}, options?: {
546+
onlySelf?: boolean;
547+
emitEvent?: boolean;
548+
}): void;
549+
}
550+
509551
// @public
510552
export class FormsModule {
511553
// (undocumented)

packages/forms/src/forms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export {FormBuilder, UntypedFormBuilder, ɵGroupElement} from './form_builder';
4545
export {AbstractControl, AbstractControlOptions, FormControlStatus, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model';
4646
export {FormArray, UntypedFormArray, ɵFormArrayRawValue, ɵFormArrayValue} from './model/form_array';
4747
export {FormControl, FormControlOptions, FormControlState, UntypedFormControl, ɵFormControlCtor} from './model/form_control';
48-
export {FormGroup, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group';
48+
export {FormGroup, FormRecord, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group';
4949
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
5050
export {VERSION} from './version';
5151

packages/forms/src/model/form_group.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,3 +584,97 @@ export type UntypedFormGroup = FormGroup<any>;
584584
export const UntypedFormGroup: UntypedFormGroupCtor = FormGroup;
585585

586586
export const isFormGroup = (control: unknown): control is FormGroup => control instanceof FormGroup;
587+
588+
export class FormRecord<TControl extends AbstractControl<ɵValue<TControl>, ɵRawValue<TControl>> =
589+
AbstractControl> extends
590+
FormGroup<{[key: string]: TControl}> {}
591+
592+
/**
593+
* Tracks the value and validity state of a collection of `FormControl` instances, each of which has
594+
* the same value type.
595+
*
596+
* `FormRecord` is very similar to {@see FormGroup}, except it enforces that all controls in the group have the same type,
597+
* and can be used with an open-ended, dynamically changing set of controls.
598+
*
599+
* @publicApi
600+
*/
601+
export interface FormRecord<TControl> {
602+
/**
603+
* Registers a control with the records's list of controls.
604+
*
605+
* {@see FormGroup#registerControl}
606+
*/
607+
registerControl(name: string, control: TControl): TControl;
608+
609+
/**
610+
* Add a control to this group.
611+
*
612+
* {@see FormGroup#addControl}
613+
*/
614+
addControl(name: string, control: TControl, options?: {emitEvent?: boolean}): void;
615+
616+
/**
617+
* Remove a control from this group.
618+
*
619+
* {@see FormGroup#removeControl}
620+
*/
621+
removeControl(name: string, options?: {emitEvent?: boolean}): void;
622+
623+
/**
624+
* Replace an existing control.
625+
*
626+
* {@see FormGroup#setControl}
627+
*/
628+
setControl(name: string, control: TControl, options?: {emitEvent?: boolean}): void;
629+
630+
/**
631+
* Check whether there is an enabled control with the given name in the group.
632+
*
633+
* {@see FormGroup#contains}
634+
*/
635+
contains(controlName: string): boolean;
636+
637+
/**
638+
* Sets the value of the `FormRecord`. It accepts an object that matches
639+
* the structure of the group, with control names as keys.
640+
*
641+
* {@see FormGroup#setValue}
642+
*/
643+
setValue(value: {[key: string]: ɵValue<TControl>}, options?: {
644+
onlySelf?: boolean,
645+
emitEvent?: boolean
646+
}): void;
647+
648+
/**
649+
* Patches the value of the `FormRecord`. It accepts an object with control
650+
* names as keys, and does its best to match the values to the correct controls
651+
* in the group.
652+
*
653+
* {@see FormGroup#patchValue}
654+
*/
655+
patchValue(value: {[key: string]: ɵValue<TControl>}, options?: {
656+
onlySelf?: boolean,
657+
emitEvent?: boolean
658+
}): void;
659+
660+
/**
661+
* Resets the `FormRecord`, marks all descendants `pristine` and `untouched` and sets
662+
* the value of all descendants to null.
663+
*
664+
* {@see FormGroup#reset}
665+
*/
666+
reset(value?: {[key: string]: ɵValue<TControl>}, options?: {
667+
onlySelf?: boolean,
668+
emitEvent?: boolean
669+
}): void;
670+
671+
/**
672+
* The aggregate value of the `FormRecord`, including any disabled controls.
673+
*
674+
* {@see FormGroup#getRawValue}
675+
*/
676+
getRawValue(): {[key: string]: ɵRawValue<TControl>};
677+
}
678+
679+
export const isFormRecord = (control: unknown): control is FormRecord =>
680+
control instanceof FormRecord;

packages/forms/test/typed_integration_spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

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

1516
describe('Typed Class', () => {
1617
describe('FormControl', () => {
@@ -636,6 +637,60 @@ describe('Typed Class', () => {
636637
});
637638
});
638639

640+
describe('FormRecord', () => {
641+
it('supports inferred records', () => {
642+
let c = new FormRecord({a: new FormControl(42, {initialValueIsDefault: true})});
643+
{
644+
type ValueType = Partial<{[key: string]: number}>;
645+
let t: ValueType = c.value;
646+
let t1 = c.value;
647+
t1 = null as unknown as ValueType;
648+
}
649+
{
650+
type RawValueType = {[key: string]: number};
651+
let t: RawValueType = c.getRawValue();
652+
let t1 = c.getRawValue();
653+
t1 = null as unknown as RawValueType;
654+
}
655+
c.registerControl('c', new FormControl(42, {initialValueIsDefault: true}));
656+
c.addControl('c', new FormControl(42, {initialValueIsDefault: true}));
657+
c.setControl('c', new FormControl(42, {initialValueIsDefault: true}));
658+
c.removeControl('c');
659+
c.removeControl('missing');
660+
c.contains('c');
661+
c.contains('foo');
662+
c.setValue({a: 42});
663+
c.patchValue({c: 42});
664+
c.reset({c: 42, d: 0});
665+
});
666+
667+
it('supports explicit records', () => {
668+
let c = new FormRecord<FormControl<number>>(
669+
{a: new FormControl(42, {initialValueIsDefault: true})});
670+
{
671+
type ValueType = Partial<{[key: string]: number}>;
672+
let t: ValueType = c.value;
673+
let t1 = c.value;
674+
t1 = null as unknown as ValueType;
675+
}
676+
{
677+
type RawValueType = {[key: string]: number};
678+
let t: RawValueType = c.getRawValue();
679+
let t1 = c.getRawValue();
680+
t1 = null as unknown as RawValueType;
681+
}
682+
c.registerControl('c', new FormControl(42, {initialValueIsDefault: true}));
683+
c.addControl('c', new FormControl(42, {initialValueIsDefault: true}));
684+
c.setControl('c', new FormControl(42, {initialValueIsDefault: true}));
685+
c.contains('c');
686+
c.contains('foo');
687+
c.setValue({a: 42, c: 0});
688+
c.patchValue({c: 42});
689+
c.reset({c: 42, d: 0});
690+
c.removeControl('c');
691+
});
692+
});
693+
639694
describe('FormArray', () => {
640695
it('supports inferred arrays', () => {
641696
const c = new FormArray([new FormControl('', {initialValueIsDefault: true})]);

0 commit comments

Comments
 (0)