Nested forms made simple
Reusable forms in Angular 15
Implement a clean reusable ControlValueAccessor with typed forms
Have you as a developer 👨💻 ever got the requirement in which you had to implement a reusable form with Angular that can be used in multiple different places? Here is a guide how to create such a nested form with low cohesion to its internal implementation on an example of the address form.
I will demonstrate you an example of how you can implement a nested address form group with the city, street and house number inputs and reuse it multiple times with just one line of code as simple form control:
Step-by-step guide
You need to create a normal FormGroup
in parent component, which has some address FormControls
which are then bound to the reusable nested form, so that all properties of nested form are in sync with parent form control.
As next you need a reusable address component that implements ControlValueAccessor and Validator interfaces. I will explain each step in detail and why you need it.
- First let’s define interfaces for our model: Company and Address. Company has its address and an array of customer addresses. Our typed form will use these interfaces.
2. Create a normal FormGroup
for the Company, which must contain a FormControl
for the company’s address and a FormArray
for customer addresses. You can have any combination and nesting of reusable address forms in your app, but for this example we use this form to bring it to the point.
3. Then you should create a component for the address form group and implement interface ControlValueAccessor in order to let this component behave like a usual form control, although it contains whole Address value. And implement interface Validator to propagate validation errors and form status to parent form control.
There are some necessary methods of ControlValueAccessor that must be implemented in this component. These methods should never be called manually in code, because they are called automatically by Angular forms API when user interacts with the form. And we should implement them to connect parent form control to the nested form:
registerOnChange(fn: any)
— registers callback function which automatically sends nested form value to parent form control. It should get called on each nested form change, so that parent control is in sync with nested form.registerTouched(fn: any)
— registers callback function which marks parent form control as touched. So that propertiestouched
,pristine
anddirty
of parent form control are synchronized with nested form group.setDisabledState(isDisabled: boolean)
— is needed to handle disabling of nested form when methodsdisable()
orenable()
are called manually on parent form control. In this case we should disable/enable the nested form depending onisDisabled
value.writeValue(value: any)
— method which is automatically called whensetValue(address)
orpatchValue(address)
are called manually on parent form control. We should define how we want to set the value into nested form. Important is that{ emitEvent: false }
should be passed, so thatvalueChanges
Observable of nested form does not emit inregisterOnChange
and does not send value to the parent form, because the value already was set into parent control through setValue() or patchValue(). Otherwise there will be an endless loop of sending form value between nested form and parent form control.
And you should implement interface Validator as well, in order to let validation of nested address form be sent to the parent form control validity and errors:
validate(control: AbstractControl)
— is called by Angular each time when parent form control is validated. So that if nested form is invalid/valid, the parent control also has the same validity state.
The significant aspect of implementing nested reusable forms is providing NG_VALUE_ACCESSOR
in order to let component behave like usual form control and NG_VALIDATORS
to register a component as validatable form control so that method validate()
is called by Angular Forms API.
forwardRef(() => AddressFormComponent)
is necessary to refer AddressFormComponent in formControl even before the AddressFormComponent is initialized.
Here is how nested address form template is implemented, just like a normal form:
And in the parent component you can use a nested form as simple as that, just by passing either [formControl]
or by passing formControlName
(if you have formGroup directive on parent DOM element):
You can then access nested address form value just by getting parent form control value in parent component:
customerAddress.value
will return an Address object, which user entered in the nested form, e.g.:
{
"city": "Vienna",
"street": "Stephansplatz",
"house": 1
}
Find a full implementation of an example in this GitHub repository: