Angular ControlValueAccessor by example

Angular ControlValueAccessor by example

At sLAB we currently develop an angular client for the sMOTIVE product. We wanted to extend some form controls with own behavior and had some issues with the ControlValueAccessor.

The ControlValueAccessor is Angulars interface to interact with a form control.

I did some investigations with plunker to address those issues.

plunker screenshot

  1. Outer component containing the form.
  2. Shows the properties of the form and the 'business model' (value).
  3. The form control implementing the ControlValueAccessor interface.
  4. These buttons trigger changes inside the control.
  5. The buttons trigger changes from the outer component.

my-input.component.ts

export class MyInputComponent implements ControlValueAccessor {  
  myValue: any = null;
  propagateChange = (_: any) => {};

  writeValue(obj: any): void {
    this.myValue = obj;
  }

  changeMe() {
    this.myValue = 'new value changed from inside';
    this.propagateChange(this.myValue);
  }

  propagateInitial() {
    this.propagateChange('initial');
  }

  propagateSomeValue() {
    this.propagateChange('some other value');    
  }
}

The above code is an excerpt of the form control.


There are four important methods:

  • writeValue(obj: any) through this method angular sets value with binding. This method is part of the ControlValueAccessor interface.
  • changeMe() changes the inner value and propagate the changed value through the registered change function.
  • propagateInitial() does not touch the inner value and calls the registered function with the value 'initial'. With this method I wanted to test if angular remembers the initial value of a control and reset the dirty/pristine flag.
  • propagateSomethingValue() does not touch the inner value and calls the registered function with a complete different value.

Independently of the form type the following facts can be noticed:

  • writeValue() has no effect on the dirty flag of the form.
  • changeMe() changes the inner value, marks the form as dirty and sets the value on the outer component.
  • propagateIntial() marks the form as dirty and sets the value of the outer component to the value 'initial'.
  • propagateSomethingValue() marks the form as dirty sets the outer value.

With this in mind, we can note some rules:

  • Do not call the registered change function inside the writeValue otherwise the form is always dirty.
  • Change the inner value and propagate the change with the registered function. Hold the inner and the outer value in sync!
  • User driven (button click, key press) actions should result in value propagation through the registered function.

template-driven forms

<form #myForm="ngForm">  
  <app-my-input [(ngModel)]="theValue" name="my-input"></app-my-input>
</form>

A template-driven form binds the value of the outer component with [(ngModel)] to the control.

export class App {  
  @ViewChild('myForm') form;

  changeThat() {
    this.theValue = 'change from outside';
  }

  resetForm() {
    this.form.reset();
    this.theValue = 'initial after reset';
  }
}
  • changeThat() sets the value on the outer component and the binding sets the value on the control.
  • resetForm() resets the formu. This changes dirty to false, pristine to true, but the control will be called with writeValue(null)! So this changes the controls value to null independently of any initial value. After calling the reset the developer is responsible to set the initial value again from the outer component.

reactive forms

<form [formGroup]="myForm">  
  <app-my-input formControlName="valueName"></app-my-input>
</form>

A template-driven form binds the value of the outer component with [(ngModel)] to the control.

export class App {

  myForm: FormGroup;
  theValue = 'initial';

  constructor() {
    this.myForm = new FormGroup({ 
      // set the model value as initial value of the control
      valueName: new FormControl(this.theValue);
    });
  }

  ngOnInit(): void { 
    this.myForm.valueChanges.subscribe(value => this.theValue = value['valueName']); 
  }

  changeThat() {
    this.myForm.setValue({valueName: 'change from outside'});
  }

  resetForm() {
    this.myForm.reset({valueName: 'initial after reset'});
  }

  markPristine() {
    this.myForm.get('valueName').markAsPristine();    
  }
}
  • constructor() creates the form with angular form classes. I didn't use the builder to show the relation between the form classes and the parts of the form.
  • ngOnInit() sets a change listener on the FormGroup. This listener is responsible to change the business model. With reactive forms this is not done automatically through binding!
  • changeThat() changes the form with setValue(). This can also be done with patchValue().
  • resetForm() resets the form. The method accepts a JSON object, which is used to initially set the form values.
  • markPristine() just resets the dirty/pristine property of a single control. All other properties of the control stay the same.

Comments