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:
import { ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
@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;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HappinessLevelComponent {
mood = 'neutral';
}
@NgModule({
declarations: [HappinessLevelComponent],
exports: [HappinessLevelComponent],
})
export class HappinessLevelComponentModule {}
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 { ChangeDetectionStrategy, Component, NgModule } 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,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HappinessLevelComponent implements ControlValueAccessor {
mood = 'neutral';
}