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 is 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? Questions or need help with Angular Reactive Forms? Let me know!
Top comments (6)
Thank Jennifer
I think its also good to clarify that the writeValue() CVA interface method isn't discharged after your onChanges() method is called. Meaning that the parent formControl (rating) doesnt update the child component property (value) when the formControl value is updated. It seemingly can only be done in the parent via calling the parent's formControl.setValue()/patchValue().
I think this is right, but please correct me because I'm making assumptions based on my own practice and I also noticed that right at the end of you child components model, the setRating() method does this manually rather than just call onChange() by itself.
No one explains this and I had to find out from looking at your code.
Hi Jennifer.
Good article!
But I'd like to see also the template part of the StarRaterComponent.
Is it visible anywhere?
Hi Daniele,
Here you go! github.com/tehfedaykin/galaxy-rati...
Good article. Wondering if the writeValue method should also invoke onChanged and onTouched.
I love your explanation! The listing of situations and such is very nice.
But you should include all the code, imports and template! :P
Very good jennifer! thanks!
You should show the child compoenent's template too, it was helpful seeing it the comments.