How to use ControlValueAccessor interface

杨旭 bio photo By 杨旭

How to use ControlValueAccessor interface

Create a switch-button component to explore this interface. There is only a button styled by bootstrap.

When clicking the button, change the style and label.

That’s all.

The template is simple enough, only contains a button that will style its class with value property and catch the click event.

<button type="button" (click)="switch()" class="btn" [ngClass]="{'btn-success': value, 'btn-warning': !value}">
  
</button>

Create our component class that implements ControlValueAccessor

import {Component, Input, OnInit} from '@angular/core';
import {ControlValueAccessor} from '@angular/forms';

@Component({
  selector: 'switch-button',
  templateUrl: './switch-button.component.html',
  styleUrls: ['./switch-button.component.scss']
})
export class SwitchButtonComponent implements ControlValueAccessor {


  @Input() value = true;

  switch() {
    this.value = !this.value;
    this.onChange();
    this.onTouch();
  }

  getLabel() {
    return this.value ? 'On' : 'Off';
  }

  writeValue(obj: any): void {
    throw new Error('Method not implemented.');
  }

  private onChange() {
    console.log('onChanged');
  };
  private onTouch() {
    console.log('onTouched');
  };

  registerOnChange(fn: any): void {
    console.log('registerOnChange');
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    console.log('registerOnTouched');
    this.onTouch = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    throw new Error('Method not implemented.');
  }
}

Four methods need to implement:

  • registerOnChange
  • registerOnTouched
  • setDisabledState
  • writeValue And we are going to find out how to work with them.

registerOnChange & registerOnTouched

As described in Angular API document, they give us functions when the control is touched and changed.

  • When registerOnChange & registerOnTouched will be called?
  • How to use the parameter we got from Angular?

We have add some logs and save the parameter to an exist method.

Then we try to load the component and click the button.

We found that the register functions have never been called, the original method is used when clicking. What’s wrong with it?

We guess that the component need to work with ngModel.

<switch-button [(ngModel)]="switchButtonValue"></switch-button>

But when loading the component, we got an error.

By checking some opensource project, we found that when working with ControlValueAccessor we need to provide a NG_VALUE_ACCESSOR

@Component({
  selector: 'switch-button',
  templateUrl: './switch-button.component.html',
  styleUrls: ['./switch-button.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SwitchButtonComponent),
      multi: true
    }
  ]
})

This provider means that when the host component need to load NG_VALUE_ACCESSOR, a SwitchButtonComponent will be provided . multi allows multiple values for a single token.

After that the methods will be called when loading the component. And the host’s class will be changed when clicking.

After clicking:

writeValue

Print the switchButtonValue in the host component for debugging.

<switch-button [(ngModel)]="switchButtonValue"></switch-button>


And we will change the value to false in the host component after 5 seconds.

  switchButtonValue = true;

  ngOnInit() {
    setTimeout(() => {
      this.switchButtonValue = false;
    }, 5000);
  }

After 5 seconds, the value in host component is changed and writeValue will be called.

  writeValue(obj: any): void {
    console.log('writeValue', obj);
    this.value = obj;
  }

But when clicking the button, the host component didn’t discover the changing.

Emit value change

Add more log to print the function passed by registerOnChange.

  registerOnChange(fn: any): void {
    console.log('registerOnChange', fn);
    this.onChange = fn;
  }

We found that the function accept a parameter named newValue, maybe it’s what we want.

Pass the new value as a parameter when calling onChange method.

  switch() {
    this.value = !this.value;
    this.onChange(this.value);
    this.onTouch();
  }

Then it works well.

The example above works well with ngModel and how about FormControl

Create a FormControl in the host component.


  switchButtonControl = new FormControl(true);

  ngOnInit() {
    setTimeout(() => {
      this.switchButtonControl.setValue(false);
    }, 5000);
  }
<switch-button [formControl]="switchButtonControl"></switch-button>
<pre>
  
</pre>

It works, and the status of control will be updated when clicking.

At last, try setDisabledState

Enable and disable it with switchButtonControl.

    setTimeout(() => {
      // this.switchButtonValue = false;
      this.switchButtonControl.disable();

      setTimeout(() => {
        this.switchButtonControl.enable();
      }, 5000)
    }, 5000);

And change the button status when enabled or disabled.

<button type="button"  (click)="switch()" [disabled]="disabled" class="btn" [ngClass]="{'btn-success': value, 'btn-warning': !value}">
  
</button>
  disabled = false;
  
  setDisabledState(isDisabled: boolean): void {
    console.log('setDisabledState', isDisabled);
    this.disabled = isDisabled;
  }

The setDisabledState is called when control’s enable/disable method is called.