Tutorial hero image
Lesson icon

Creating Custom Form Input Components with ControlValueAccessor

4 min read

Originally published July 19, 2018

A while ago I wrote a tutorial on building a custom input component for an Ionic application. It used an HTML5 <canvas> and a gesture listener to create a rating/satisfaction input component that looks like this:

It was just a bit of a silly/fun component to serve as an example for learning about custom components, using gesture listeners, and interacting with a canvas in an Ionic application. Something like this could legitimately be used as part of a form, or some other custom component that would act as a non-typical style input for a form. However, there is a problem with using custom components in forms.

The default inputs we are familiar with (like text, textarea, select, and so on), and even Ionic's own input components, are compatible with Angular's form controls. This means that we can attach ngModel:

<ion-input type="text" [(ngModel)]="myValue"></ion-input>

or we can set up form controls:

<ion-input type="text" formControlName="myControl"></ion-input>

To utilise Angular's powerful form controls. If you are unfamiliar with using FormBuilder, FormGroup, and Validators in Angular forms, I would recommend reading Advanced Forms & Validation in Ionic. In short, using the tools that Angular provides us to build and interact with forms makes our lives a lot easier.

If we want to integrate a custom component that we have built as part of a form, we would want to do something like this:

<app-smile-input formControlName="rating"></smile-input>

or

<app-smile-input [(ngModel)]="rating"></smile-input>

But that isn't going to work. Angular allows you to update form inputs, to receive notifications when they change, to set a disabled state, to grab the current value, and more. Since this is a custom component we have built, how is Angular supposed to know where to grab the value from? or where to update the value? or when the value changes?

Outline

Introducing ControlValueAccessor

Angular simply will not know how your custom component works, and so won't be able to figure out those things I just mentioned. This is where ControlValueAccessor comes in. The documentation (and even the name) looks somewhat intimidating and confusing, but the idea is pretty straight-forward.

According to the documentation a ControlValueAccessor acts as a bridge between the Angular Forms API and a native element in the DOM. We can use ControlValueAccessor in our custom components to let the Angular forms API know how to update the value in our component, when the input has been updated, and so on.

To do that, we will need to implement a few functions. The interface that needs to be implemented looks like this:

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

Our custom component will need to implement each of these functions:

  • writeValue
  • registerOnChange
  • registerOnTouched
  • setDisabledState (optional)

The writeValue method will be used by the Angular forms API to modify the value of the component. The registerOnChange function will allow Angular to provide our component with a function (i.e. Angular calls this function, to pass us its own function) that we can make a call to when a change occurs. The same goes for the registerOnTouched function. The setDisabledState method will be called by Angular to disable our input if you want to implement this you might make some graphical change to the component to indicate that it is disabled.

Implementing this ControlValueAccessor interface is basically a way to give Angular the functionality it needs to treat your custom component like any other kind of standard form input.

Let's start working on implementing this.

Implementing ControlValueAccessor

First, let's take a look at a very basic custom input component that does not yet implement ControlValueAccessor:

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

@Component({
  selector: 'app-random-input',
  templateUrl: './random-input.component.html',
  styleUrls: ['./random-input.component.scss'],
})
export class RandomInputComponent {
  public randomNumber: number;

  constructor() {}

  setRandomNumber() {
    this.randomNumber = Math.random();
  }
}

I've kept this component intentionally simple so that it is easier to demonstrate the ControlValueAccessor interface. This custom input component will allow the user to click a button that calls setRandomNumber to set randomNumber to a random number. The template for this component just looks like this:

<p>{{ randomNumber }}</p>
<ion-button color="light" (click)="setRandomNumber()">Set Number</ion-button>

We know that the value we are interested in is randomNumber and that if you wanted to update the value of the input you would just change randomNumber but Angular does not know that. Now let's take a look at the component with ControlValueAccessor implemented:

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

@Component({
  selector: 'app-random-input',
  templateUrl: './random-input.component.html',
  styleUrls: ['./random-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: RandomInputComponent,
      multi: true,
    },
  ],
})
export class RandomInputComponent implements ControlValueAccessor {
  public randomNumber: number;

  disabled = false;
  onChange = (randomNumber: number) => {};
  onTouch = () => {};

  setRandomNumber(): void {
    this.writeValue(Math.random());
    this.onTouch();
  }

  // Allow Angular to set the value on the component
  writeValue(value: number): void {
    this.onChange(value);
    this.randomNumber = value;
  }

  // Save a reference to the change function passed to us by
  // the Angular form control
  registerOnChange(fn: (randomNumber: number) => void): void {
    this.onChange = fn;
  }

  // Save a reference to the touched function passed to us by
  // the Angular form control
  registerOnTouched(fn: () => void): void {
    this.onTouch = fn;
  }

  // Allow the Angular form control to disable this input
  setDisabledState(disabled: boolean): void {
    this.disabled = disabled;
  }
}

First, we've added the NG_VALUE_ACCESSOR provider to the decorator for our component this is necessary for implementing the ControlValueAccessor interface, and we have indicated that the RandomInputComponent "implements" ControlValueAccessor. We have also added those four functions we were talking about.

Notice that registerOnChange is passed a function, and then we set that function on this.onChange. Angular will call registerOnChange in our component and it will pass the component a function, we then save a reference to that function so that we can call it later (when a change occurs). We are doing the exact same thing for registerOnTouched.

The setDisabledState will simply be called when the input is being disabled we are just toggling a flag to represent the disabled state, and we could then use that to impact the component in some way.

The most important part is that we are now modifying the randomNumber class member by passing a value to the writeValue method. This will allow Angular to call this method to set its own value if needed. Since we have made this change, we also modify our setRandomNumber function that is being called from our template to use the writeValue method as a means for changing the value too.

We will now be able to use this custom component with the Angular forms API in the same way that we would use any other default or Ionic input component.

Summary

You could have any number of crazy things happening in your component maybe you are even communicating with Bluetooth, taking photos, making calls to a server but in the end, the component is going to be responsible for supplying a single value (or maybe it is an array of values or an object). As long as we implement this interface, Angular will be able to understand how to interact with your component, and you will be able to use all of the benefits that the Angular forms API has to offer.

If you enjoyed this article, feel free to share it with others!