DEV Community

Jennifer Wadella for Bitovi

Posted on • Updated on

Understanding Angular's Control Value Accessor Interface

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.

star rating input

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!

Discussion (6)

Collapse
nickpea profile image
NickPea

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.

Collapse
danielebarell profile image
Daniele Barell

Hi Jennifer.
Good article!
But I'd like to see also the template part of the StarRaterComponent.
Is it visible anywhere?

Collapse
likeomgitsfeday profile image
Jennifer Wadella Author

Hi Daniele,

Here you go! github.com/tehfedaykin/galaxy-rati...

Collapse
ren318 profile image
decoder318

Good article. Wondering if the writeValue method should also invoke onChanged and onTouched.

Collapse
lakerko profile image
Martin Ockovsky

I love your explanation! The listing of situations and such is very nice.
But you should include all the code, imports and template! :P

Collapse
elazarza profile image
elazarza

Very good jennifer! thanks!
You should show the child compoenent's template too, it was helpful seeing it the comments.