Angular 2+ Reactive Forms - Control Value Accessor Intro

May 15, 2018

Complicated Problem 3: I need to display one thing in an input field for the user but submit something else

A common use case for this problem is that the library I’m using doesn’t play nice with reactive forms.

Here enters our hero, the Control Value Accessor. It’s not as scary as it sounds, it’s basically a way to build a custom input. From the docs: Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM.

CVA Interface

export interface ControlValueAccessor {
  writeValue(obj: any) : void
  registerOnChange(fn: any) : void
  registerOnTouched(fn: any) : void
}

There are three required methods:

  • writeValue writes new value to the element.,
  • registerOnChange registers a callback function that is called when the control’s value changes in the UI
  • registerOnTouched registers a callback function is called by the forms API on initialization to update the form model on blur.

Avril Lavigne

Using the CVA is great for granular control of displaying to the UI and communicating with the forms API.

CVA Key Concepts

  • Keep your wrapper components dumb.
  • Just input and output form values!
  • Leave validation logic to the parent form component.

Back to our initial problem

Avril Lavigne

I want to display the name in my typeahead and submit the id, but my library doesn’t allow for that.

//my sample data for my dropdown
[{
  name: 'breakfast',
  id: 1
},{
  name: 'lunch',
  id: 2
},
{
  name: 'appetizer',
  id: 3
}];

Let’s wrap it in a component that implements the CVA interface.

Avril Lavigne

First we build our wrapper component. This is a component we’ll use to wrap our implementation of our typeahead libary and extend the CVA interface to. This component will be implemented as a directive in our parent form.

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

@Component({
  selector: 'my-typeahead',
  templateUrl: './typeahead.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyTypeaheadComponent),
      multi: true
    }
  ]
})
export class MyTypeaheadComponent implements ControlValueAccessor, OnInit {
  @Input() data: any[];
  @Input('value') _value = null;
  onChange: any = () => { };
  onTouched: any = () => { };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
  }

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

  registerOnTouched( fn : any ) : void {
    this.onTouched = fn;
  }

  writeValue(value) {
    this.value = value;
  }
}

The HTML of our component (using the ngx-bootstrap typeahead component):

<ng-template #itemTemplate let-model="item">
      {{model[displayVal]}}
    </ng-template>
<input
  class="form-control"
  [(ngModel)]="selected"
  [typeahead]="data"
  [typeaheadOptionField]="displayVal"
  [typeaheadItemTemplate]="itemTemplate"
  [typeaheadMinLength]="0"
  (typeaheadOnSelect)="onSelect($event)"
  (blur)="onBlur($event)"
>

As mentioned we hook our component into our form:

<form [formGroup]="mealForm">
  <div class="form-group">
    <label for="recipe">Recipe</label>
    <my-typeahead formControlName="recipe" [data]="recipes"></my-typeahead>
  </div>
</form>

Ok, sweet, but what if I need to prepopulate a value cause I’m editing an existing form?

export class MyTypeaheadComponent implements ControlValueAccessor, OnInit {
  @Input() data: any[];

  //check me out!!!
  @Input('value') _value;
  //I'm the value of the input and can be manipulated however you like!
  //notice we don't even have to bind this from our directive implementation
  //it gets inherited from the form control

  onChange: any = () => { };
  onTouched: any = () => { };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
  }

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

  registerOnTouched( fn : any ) : void {
    this.onTouched = fn;
  }

  writeValue(value) {
    this.value = value;
  }
}

CVA: Good for more than just wrapping random library components!

Avril Lavigne

You can use the CVA interface to create reusable form elements instead of drowning in event emitter soup!

Demo code available at https://github.com/tehfedaykin/complicated-forms-app