How to Make a Component Compatible with Angular Forms?

The Angular framework provides 2 ways of creating forms:

  1. Reactive Forms
  2. Template Driven

The content of this article is valid to both of them.

Control Value Accessor (Interface)

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}

So, this is an interface provided by Angular that will allow us to make our components compatibles with Angular Forms.

NG_VALUE_ACCESSOR (InjectionToken)

This element is essential as part of implementing a form-compatible component. Its usage is mainly to register the component. More info

Component

For the aim of this example, let's imagine that we want to build a component that allows you to select your mood, just like this:

Component Implementation

Component Code:

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export enum Mood {
  Red = 'red',
  Green = 'green',
}

@Component({
  selector: 'app-custom-component',
  templateUrl: './custom-component.component.html',
  styleUrls: ['./custom-component.component.scss'],
  providers: [
    // This part is very important to register the class as a ControlValueAccessor one
    {
      provide: NG_VALUE_ACCESSOR,
      // This reference the class that implements Control Value Accessor
      useExisting: forwardRef(() => CustomComponentComponent),
      multi: true,
    },
  ],
})
export class CustomComponentComponent implements ControlValueAccessor {
  /* Reference to the Enum to be used in the template */
  readonly moodRef = Mood;
  disable: boolean = false;
  selected: Mood = Mood.Green;

  updateState(selectedItem: Mood): void {
    this.selected = selectedItem; // Updating internal state
    this.onChange(this.selected); // 'publish' the new state
  }

  /***********************************************************************
   * Control Value Accessor Implementation
   ***********************************************************************/

  private onChange: any;
  private onTouch: any;

  // Invoked by angular - update internal state
  writeValue(obj: any): void {
    this.selected = obj;
  }

  // Invoked by angular - callback function for changes
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  // Invoked by angular - callback function for touch events
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // Invoked by angular - update disabled state
  setDisabledState?(isDisabled: boolean): void {
    this.disable = isDisabled;
  }
}

Template code:

<p>How do you feel?</p>

<ng-container *ngIf="!disable; else disabledTemplate">
  <button
    [ngClass]="{
      custom__button__red: true,
      'custom__button--selected': selected === moodRef.Red
    }"
    (click)="updateState(moodRef.Red)"
  >
    Red
  </button>
  <button
    [ngClass]="{
      custom__button__green: true,
      'custom__button--selected': selected === moodRef.Green
    }"
    (click)="updateState(moodRef.Green)"
  >
    Green
  </button>
</ng-container>

<ng-template #disabledTemplate>
  <p>I'm disabled</p>
</ng-template>

SCSS:

.custom__button {
  &__red {
    background-color: red;
  }
  &__green {
    background-color: green;
  }

  &--selected {
    margin: 1em;
    border: solid 5px black;
  }
}

Reactive Form usage

The component is compatible with the directives: formControlName and formControl.

<form [formGroup]="formGroup">
  <app-custom-component
    [formControlName]="controlsRef.Mood"
  ></app-custom-component>
</form>

Template Driven Form usage

The component is also compatible with ngModel property:

<form>
  <app-custom-component
    [disabled]="disabled"
    [(ngModel)]="selectedMood"
    [ngModelOptions]="{ standalone: true }"
  ></app-custom-component>
</form>

Full example

The detailed implementation is in one of my Github repos:

28