Implement a custom FormControl in Angular with ControlValueAccessor
In this article I wanna show you how to create a custom FormControl component in Angular which you can use in a Reactive Forms form.
In this example we create a component in which the user can select one or more tags by clicking on them. We will also render the form value in the HTML.
This implementation is kept minimal to make the concept easier to understand. Advanced optimizations and features have been omitted to avoid distraction, but feel free to expand on this foundation for real-world use cases.
Preview
FormControl component
First we create a component which will be the FormControl.
The component needs to implement ControlValueAccessor
. This is an interface that acts as a bridge between the Angular Forms API and the input component. See Angular.dev documentation
When implementing this interface, these methods should be created:
writeValue(value: any)
- This method will be called when the Reactive Forms API writes a value (from the form model) to this FormControl.registerOnChange(fn: any)
- This method is called when the component is created. It gives a callback function, which we (this FormControl component) need to call when we update the value and need to let the Reactive Forms API know that it has been updated.registerOnTouched(fn: any)
- This method is called when the component is created. It gives a callback function, which we (this FormControl component) need to call when the user touches this FormControl.setDisabledState(isDisabled: boolean)
- This method is called when the component is created and it is optional. It contains a property which tells us that Reactive Forms API marked this FormControl as 'enabled' or 'disabled'.
We also need to add a provider, so our component gets access to the necessary NG_VALUE_ACCESSOR
.
Component code file
The component implements the ControlValueAccessor
interface and uses the NG_VALUE_ACCESSOR
provider.
import { NgClass } from '@angular/common';
import { Component, forwardRef, input, signal } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-tags',
templateUrl: './tags.component.html',
styleUrl: './tags.component.css',
standalone: true,
imports: [
NgClass
],
providers: [
// Add this provider which references this component (see 'TagsComponent' in useExisting property).
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TagsComponent),
multi: true
}
]
})
export class TagsComponent implements ControlValueAccessor {
// This FormControl component has a 'tags' property, so it can retrieve the tags from a parent component.
readonly tags = input<string[]>();
// Holds the actual value of the FormControl.
readonly myValue = signal<string[]>([]);
// Holds the state of whether this FormControl is disabled.
readonly disabled = signal<boolean>(false);
// References to the onChange and onTouched callback methods, given by the Reactive Forms API.
private _onChangeFn = (_value: string[]) => {}
private _onTouchedFn = () => {};
// Register callback function for when this FormControl value has been updated.
registerOnChange(fn: any): void {
this._onChangeFn = fn;
}
// Register callback function for when this FormControl has been touched.
registerOnTouched(fn: any): void {
this._onTouchedFn = fn;
}
// This method will be called when this FormControl value needs to be updated.
// The given value is from the (parent) form model.
writeValue(value: string[]): void {
if (!value) {
this.myValue.set([]);
return;
}
if (!Array.isArray(value)) {
this.myValue.set([value]);
return;
}
this.myValue.set(value);
}
// This method will be called when this FormControl is marked as 'enabled' or 'disabled'.
setDisabledState?(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
// Our own method which gets called when the user clicks on a tag.
onTagClick(tag: string): void {
if (this.disabled()) {
return;
}
// 1) Update our value
if (this.myValue().includes(tag)) {
this.myValue.update((v) => v.filter((x) => x !== tag));
} else {
this.myValue.update((v) => [...v, tag]);
}
// 2) Call the callback methods to tell Reactive Forms API about the updated value.
this._onChangeFn(this.myValue());
this._onTouchedFn();
}
}
Component template file
The template renders the tags, which the user can click on to select them. A selected tag will have a 'selected' class.
The container div will have a 'disabled' class when this FormControl is disabled (by the Reactive Forms API).
<div class="tags" [ngClass]="{ 'disabled': disabled() }">
<ul>
@for (tag of tags(); track tag) {
<li [ngClass]="{ 'selected': myValue().includes(tag) }" (click)="onTagClick(tag)">{{ tag }}</li>
}
</ul>
</div>
Component CSS file
We show the tags as clickable button-labels next to each other.
div.tags {
ul {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0;
margin: 0;
list-style: none;
li {
display: block;
padding: 4px 12px;
border: 1px solid #dbdbdb;
border-radius: 3px;
margin: 0;
background-color: #eeeeee;
user-select: none;
cursor: pointer;
&.selected {
border-color: #4e87bd;
background-color: #c0dffd;
}
}
}
&.disabled {
opacity: 0.6;
ul {
li {
cursor: not-allowed;
}
}
}
}
Form component
Now its time to create a (parent) component which contains a Reactive Forms form.
Component code file
This component contains the form with a field called 'cities'. It also has a list of cities.
import { Component } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TagsComponent } from '../tags/tags.component';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrl: './parent.component.scss',
standalone: true,
imports: [
FormsModule,
JsonPipe,
ReactiveFormsModule,
TagsComponent
]
})
export class ParentComponent {
form = new FormGroup({
cities: new FormControl<string[]>([], []),
});
cities = [
'Berlin',
'Copenhagen',
'Dublin',
'Lisbon',
'London',
'Madrid',
'Oslo',
'Paris',
'Prague',
'Rome'
];
}
Component template file
The HTML of the component shows the form. For the 'cities' form field we use our custom <app-tags />
FormControl component.
The custom FormControl component has a FormControlName
property which contains the name 'cities'. Now this custom FormControl is mapped to the 'cities' FormControl in de FormGroup definition (in the code file).
We also give the custom FormControl component the cities
array, which will be rendered as tags.
We print the form.value
as JSON in HTML, so we can look at the actual form data.
<form [formGroup]="form" novalidate>
<div class="field">
<label>Select cities</label>
<app-tags formControlName="cities" [tags]="cities" />
</div>
</form>
<pre>{{ form.value | json }}</pre>
Conclusion
We have created a custom FormControl. To be able to use a component as a custom control, we needed to use the ControlValueAccessor
and implements its methods. We now can use this FormControl component for every form-field we want.
References
Angular.dev reference about setting up a Reactive Forms form
Angular.dev reference to definition of the ControlValueAccessor interface