How to Make a Component Compatible with Angular Forms?

The Angular framework provides 2 ways of creating forms:
  • Reactive Forms
  • 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:

    42

    This website collects cookies to deliver better user experience

    How to Make a Component Compatible with Angular Forms?