How to implement ControlValueAccessor in Angular

Angular is a TypeScript-based open-source web application framework led by the Angular Team at Google and a community of individuals and corporations. In this article, we’ll explore how to create a custom control in Angular that can be used as a reactive form control. Angular provides extensive extensibility features to enhance the behavior of the framework.

To create a custom control, we need to implement the ControlValueAccessor interface, which includes the following four methods:

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

For this article, we’ll create a custom spinner control. Let’s start by creating a new Angular application using the following command:
ecuting the following command

ng new app_name

Next, create a component called SpinnerControl and paste the following code. The code is self-explanatory and creates the custom spinner control.

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

@Component({
  selector: "app-number",
  template: `
    <div class="container">
      <div class="left">
        <input
          class="number-input"
          [value]="value"
          type="number"
          [disabled]="disabled"
        />
      </div>
      <div class="right">
        <button (click)="onIncrement()">+</button>
        <button (click)="onDeincrement()">-</button>
      </div>
    </div>
  `,
  styles: [
    `
      .container {
        display: flex;
        padding: 0px;
      }
      .right {
        display: flex;
        flex-direction: column;
      }
      .number-input {
        height: 40px;
      }

      button {
        height: 20px;
        width: 20px;
        border: 0px;
        background-color: #aa33dd;
      }
    `
  ]
})
export class SpinnerControl implements ControlValueAccessor {
  @Input()
  public value: number;
  @Input()
  public disabled: boolean;

  onChanged: any = () => {};
  onTouched: any = () => {};

  writeValue(val): void {
    console.log(val);
    this.value = +val || 0;
  }

  registerOnChange(fn: any) {
    this.onChanged = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean) {
    this.disabled = isDisabled;
  }

  numberChange($event) {
    this.onTouched();
    this.onChanged($event.target.value);
    this.setDisabledState(this.disabled);
  }

  onIncrement() {
    this.value = this.value + 1;
    this.onChanged(this.value);
  }

  onDeincrement() {
    this.value = this.value - 1;
    this.onChanged(this.value);
  }
}
Method Name Description
writeValue Write a new value to the element
registerOnChange Is called when the control values change in the UI
registerOnTouched Is called by the forms API to update the form model on blur

In the above code, we’ve created the SpinnerControl component that implements the ControlValueAccessor interface. It includes the necessary methods to handle the control’s value, changes, and disabled state. The template defines the UI elements of the spinner control.

How to use?

Before using this custom reactive form control, you have to register this with angular providers array. See the below code for registration

providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SpinnerControl),
      multi: true
    }
  ]
  • NG_VALUE_ACCESSOR: NG_VALUE_ACCESSOR is a predefined Angular token that represents the accessor for writing and reading the value of a control. It is used to connect the custom control to the Angular forms API.
  • forwardRef: forwardRef is a function that is used to refer to a class that is not yet defined. In this case, it allows us to reference the SpinnerControl class before it is fully defined.
  • multi: multi is a boolean flag that specifies whether the provider is a multi-provider or a single-provider. In this case, we set it to true to indicate that there can be multiple providers for NG_VALUE_ACCESSOR.

By including the NG_VALUE_ACCESSOR provider in the providers array, we register the SpinnerControl as a valid control that can be used within Angular’s reactive forms.

Final code looks like below.

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

@Component({
  selector: "app-number",
  template: `
    <div class="container">
      <div class="left">
        <input
          class="number-input"
          [value]="value"
          type="number"
          [disabled]="disabled"
        />
      </div>
      <div class="right">
        <button (click)="onIncrement()">+</button>
        <button (click)="onDeincrement()">-</button>
      </div>
    </div>
  `,
  styles: [
    `
      .container {
        display: flex;
        padding: 0px;
      }
      .right {
        display: flex;
        flex-direction: column;
      }
      .number-input {
        height: 40px;
      }

      button {
        height: 20px;
        width: 20px;
        border: 0px;
        background-color: #aa33dd;
      }
    `
  ],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SpinnerControl),
      multi: true
    }
  ]
})
export class SpinnerControl implements ControlValueAccessor {
  @Input()
  public value: number;
  @Input()
  public disabled: boolean;

  onChanged: any = () => {};
  onTouched: any = () => {};

  writeValue(val): void {
    console.log(val);
    this.value = +val || 0;
  }

  registerOnChange(fn: any) {
    this.onChanged = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean) {
    this.disabled = isDisabled;
  }

  numberChange($event) {
    this.onTouched();
    this.onChanged($event.target.value);
    this.setDisabledState(this.disabled);
  }

  onIncrement() {
    this.value = this.value + 1;
    this.onChanged(this.value);
  }

  onDeincrement() {
    this.value = this.value - 1;
    this.onChanged(this.value);
  }
}

To use the custom control, you need to register it with the Angular providers array in the component where you want to use it. Here’s an example of how to register the SpinnerControl:

import { Component } from "@angular/core";

@Component({
  selector: "app-my-component",
  template: `
    <form [formGroup]="myForm">
      <app-spinner formControlName="spinnerControl"></app-spinner>
    </form>
  `
})
export class MyComponent {
  // ...
}

In the above code, we’re using the app-spinner component as a form control by binding it to the spinnerControl form control name.

That’s it! You’ve successfully created a custom spinner control in Angular that can be used as a reactive form control. The control implements the necessary ControlValueAccessor methods to integrate with Angular’s forms API.

Further reading

What is forwardRef

From Angular’s API docs on forwardRef:

forwardRef is used when the token which we need to refer to for the purposes of DI is declared, but not yet defined. It is also used when the token which we use when creating a query is not yet defined.

Please do not post any spam link in the comment box😊

إرسال تعليق (0)
أحدث أقدم