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:
- How to update the current value of the input (e.g. if we were to call
setValue
on the form control) - When the value has been changed
- 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
}
NOTE: The last function setDisabledState
is optional
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';
}