<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Frontend development
Loading

Angular |

Understanding Angular's Control Value Accessor Interface

If you're dealing with Angular forms on a regular basis one of the most powerful things you can learn is how to use the Control Value Accessor interface.

Jennifer Wadella

Jennifer Wadella

Twitter Reddit

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 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!