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

Example custom FormControl tags component

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