Skip to content

Creating Custom Controls

The last scenario we are going to cover is creating your own custom form inputs. We can use standard HTML inputs in our Angular forms:

<input formControlName="name" type="text" />

If we are using a component library, like Ionic for example, then we might also use custom inputs that look like this:

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

Now, these clearly aren’t standard HTML form controls, so how is it that Angular is still able to treat these custom components as if they were normal form controls?

Introducing the ControlValueAccessor

This is one of those things that sounds intimidating, but when you break it down it makes more sense. The ControlValueAccessor is an interface we can implement, just like we have implemented interfaces like PipeTransform for creating pipes or AsyncValidator for creating an asynchronous validator.

The point of implementing ControlValueAccessor for a component is that it tells Angular how to treat this component as a normal form input that works with both reactive forms (what we have been using) and template driven forms (e.g. [(ngModel)]). Specifically, implementing this interface for a component will let Angular know:

  1. How to update the current value of the input (e.g. if we were to call setValue on the form control)
  2. When the value has been changed
  3. When the control has been interacted with

We can implement whatever kind of wacky form input we want — as long as we implement this interface that lets Angular know how it should treat it within a form. Simple time pickers? Boring! If we want we could implement a component that uses an SVG of a blazing sun and allow the user to drag this giant fireball across the time in order to set the time of day! That’s probably a terrible idea, but my point is that you could do it, and you could treat that ridiculous control like any other standard input that is compatible with things like formControlName and [(ngModel)].

Let’s take a closer look at what this interface looks like:

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

It is probably easier to walk through this interface with an actual example.

Creating a Component that Uses ControlValueAccessor

We are going to implement a simple component that has three buttons:

  • Sad
  • Neutral
  • Happy

The user will be able to click one of these and that should be the value that Angular forms uses. Basically, this is a slightly worse implementation of a standard radio input, but it makes for an easier to follow example.

First, let’s create the component without any consideration of how it will work within a form:

@Component({
selector: 'app-happiness-level',
template: `
<div>
<button (click)="mood = 'sad'" [class.active]="mood === 'sad'">
Sad
</button>
<button (click)="mood = 'neutral'" [class.active]="mood === 'neutral'">
Neutral
</button>
<button (click)="mood = 'happy'" [class.active]="mood === 'happy'">
Happy
</button>
</div>
`,
styles: [
`
.active {
font-weight: bold;
}
`,
],
})
export class HappinessLevelComponent {
mood = 'neutral';
}

We have three buttons we can click and they will change the mood value. Whichever value is currently selected will be bold. Now let’s add in the ControlValueAccessor interface:

import { Component } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-happiness-level',
// ...snip
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: HappinessLevelComponent,
multi: true,
},
],
})
export class HappinessLevelComponent implements ControlValueAccessor {
mood = 'neutral';
}

We have added ControlValueAccessor but we have not implemented its interface in our component yet, so our code editor is going to complain that it is not implemented correctly.

An important thing to note here is what is going on in the providers:

providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: HappinessLevelComponent,
multi: true,
},
],

The NG_VALUE_ACCESSOR is a token provided by Angular, and it is what is going to allow us to notify Angular that we want to use this component with Angular forms. We supply our HappinessLevelComponent and importantly we use multi. What multi does is it will allow us to add our HappinessLevelComponent to the NG_VALUE_ACCESSOR token, rather than overwriting what that token is.

Now we can move on to implementing the interface, let’s start with writeValue:

export class HappinessLevelComponent implements ControlValueAccessor {
mood = 'neutral';
writeValue(value: 'sad' | 'neutral' | 'happy') {
this.mood = value;
}
}

We can think of this as a way for Angular to talk to our component. This is a way for Angular to let us know that it wants to set the value of our component. So, we take the value that is passed in, and we do whatever is needed to reflect that value in our component. In this case, we just change the mood to whatever value was passed in.

Next up, registerOnChange:

import { Component } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
type Mood = 'sad' | 'happy' | 'neutral';
@Component({
selector: 'app-happiness-level',
template: `
<div>
<button (click)="setMood('sad')" [class.active]="mood === 'sad'">
Sad
</button>
<button (click)="setMood('neutral')" [class.active]="mood === 'neutral'">
Neutral
</button>
<button (click)="setMood('happy')" [class.active]="mood === 'happy'">
Happy
</button>
</div>
`,
styles: [
`
.active {
font-weight: bold;
}
`,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: HappinessLevelComponent,
multi: true,
},
],
})
export class HappinessLevelComponent implements ControlValueAccessor {
mood = 'neutral';
onChange = (value: Mood) => {};
setMood(mood: Mood) {
this.mood = mood;
this.onChange(mood);
}
writeValue(value: Mood) {
this.mood = value;
}
registerOnChange(fn: () => void): void {
this.onChange = fn;
}
}

The writeValue method deals with Angular talking to our component, this method (and the next) deals with us talking back to Angular. With registerOnChange we want to let Angular know when the value in our component has changed. For example, if the user clicks on Sad then Angular needs to know that the value has changed to Sad.

What Angular does to handle this is through the registerOnChange method, which it will use to pass us a function. We can then use that function to notify Angular of changes whenever they happen. Our current set up for changing values is a bit awkward for this, so we refactored setting the mood into a method so that we only need to call this function once. Notice that we have updated the template to use this new method.

The final thing we need to implement is registerOnTouched which is basically the exact same thing — we just trigger it when this input has been “touched” rather than when the value has changed. In our case, those two things happen at the same time:

export class HappinessLevelComponent implements ControlValueAccessor {
mood = 'neutral';
onChange = (value: Mood) => {};
onTouch = () => {};
setMood(mood: Mood) {
this.mood = mood;
this.onChange(mood);
this.onTouch();
}
writeValue(value: Mood) {
this.mood = value;
}
registerOnChange(fn: () => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouch = fn;
}
}

Using the Custom Component that uses ControlValueAccessor

Now we get to the fun part — using it in a form. Fortunately, this is also the easy part because it is no different than using any other input now. We add a control for it to our group:

myForm = this.fb.nonNullable.group(
{
username: ['', Validators.required, usernameAvailableValidator],
age: [null, adultValidator],
password: ['', [Validators.minLength(8), Validators.required]],
confirmPassword: ['', [Validators.required]],
guests: this.fb.array([]),
happiness: ['neutral', Validators.required],
},
{
validators: [passwordMatchesValidator],
}
);

and then add it inside of our form:

<app-happiness-level formControlName="happiness"></app-happiness-level>

When we submit the form, we should be able to see the happiness level!

Taking it further

Most of the simple use cases are already covered by standard form inputs. If you would like to see something a bit more advanced, I have a video where I walk through creating a custom checkbox group component that implements ControlValueAccessor. You can check it out here.