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
setValueon 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}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.