If you're dealing with forms in Angular on a regular basis one of the most powerful things you can learn is how to use the Control Value Accessor interface. The CVA interface is a bridge between FormControls and their elements in the DOM. A component extending the CVA interface can create a custom form control that behaves the same as a regular input or radio button.
Why Would You Want to Use the Control Value Accessor Interface?
Sometimes you may need to create a custom form element that you want to be able to use as a regular FormControl. (For a better understanding of FormControls and other Angular Form classes you might want to read my article here). For example, creating a 5 star rating UI that updates a single value. We'll use this example in our demo.
There's a lot happening in the UI here - stars changing colors as they're hovered over and displaying different text for each ratings, but all we care about is saving a number value 0-5.
Implementing the CVA
To use the CVA interface in a component, you must implement its three required methods: writeValue
, registerOnChange
, and registerOnTouched
. There is also an optional method setDisabledState
.
The writeValue
method is called in 2 situations:
- When the formControl is instantiated
rating = new FormControl({value: null, disabled: false})
- When the formControl value changes
rating.patchValue(3)
The registerOnChange
method should be called whenever the value changes - in our case, when a star is clicked on.
The registerOnTouched
method should be called whenever our UI is interacted with - like a blur event. You may be familiar with implementing Typeaheads from a library like Bootstrap or NGX-Bootstrap that has an onBlur
method.
The setDisabledState
method is called in 2 situations:
- When the formControl is instantiated with a disabled prop
rating = new FormControl({value: null, disabled: false})
- When the formControl disabled status changes
rating.disable();
rating.enable();
A star rating component implementing the CVA may look something like this:
export class StarRaterComponent implements ControlValueAccessor {
public ratings = [
{
stars: 1,
text: 'must GTFO ASAP'
},
{
stars: 2,
text: 'meh'
},
{
stars: 3,
text: 'it\'s ok'
},
{
stars: 4,
text: 'I\'d be sad if a black hole ate it'
},
{
stars: 5,
text: '10/10 would write review on Amazon'
}
]
public disabled: boolean;
public ratingText: string;
public _value: number;
onChanged: any = () => {}
onTouched: any = () => {}
writeValue(val) {
this._value = val;
}
registerOnChange(fn: any){
this.onChanged = fn
}
registerOnTouched(fn: any){
this.onTouched = fn
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
setRating(star: any) {
if(!this.disabled) {
this._value = star.stars;
this.ratingText = star.text
this.onChanged(star.stars);
this.onTouched();
}
}
}
You must also tell Angular that your component implementing the CVA is a value accessor(remember, interfaces aren't compiled in TypeScript) using NG_VALUE_ACCESSOR and forwardRef.
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'gr-star-rater',
templateUrl: './star-rater.component.html',
styleUrls: ['./star-rater.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRaterComponent),
multi: true
}
]
})
export class StarRaterComponent implements ControlValueAccessor {
...
Using Your New CVA Component
Now, to use your fancy new CVA component, you can treat it as a plain old FormControl.
this.galaxyForm = new FormGroup({
rating: new FormControl({value: null, disabled: true})
});
<form [formGroup]="galaxyForm" (ngSubmit)="onSubmit()">
<h1>Galaxy Rating App</h1>
<div class="form-group">
<label>
Rating:
<gr-star-rater formControlName="rating"></gr-star-rater>
</label>
</div>
<div class="form-group">
<button type="submit">Submit</button>
</div>
</form>
Tada! Not so scary, huh? Need help managing other complicated form situations in your application? We're available for training or for hire, just let us know what you need help with!