Never again be confused when implementing ControlValueAccessor in Angular forms

If you’re working on a complex project, you’ll need a custom form control that implements the ControlValueAccessor interface. There are plenty of articles on the web describing how to implement this interface, but very little is said about what role it plays in the Angular forms architecture. If you want to know not just how, but why, this article is right up your street.

First, I’ll explain why the ControlValueAccessor interface is needed and how it is used in Angular. Then I’ll show you how to encapsulate third-party components as Angular components and how to use input/output mechanisms to communicate between components. We’ll show you how to use ControlValueAccessor to implement a new data communication mechanism for Angular forms.

FormControl and ControlValueAccessor

If you’ve worked with Angular forms before, you’re probably familiar with FormControl, which is described in the official Angular documentation as an entity object that tracks the value and validity of a single FormControl. Understand that whether you use template-driven or model-driven forms, FormControl will always be created. If you use reactive forms, you need to explicitly create a FormControl object and bind native controls using the FormControl or formControlName directives. If you use the template-driven approach, the FormControl object is implicitly created by the NgModel directive:

@Directive({
  selector: '[ngModel]... '. })export class NgModel ... {
  _control = new FormControl();   <---------------- here
Copy the code

Whether the formControl is created implicitly or explicitly, it must interact with native DOM form controls such as input and textarea, and most likely you’ll need to customize a formControl as an Angular component instead of using a native formControl. Custom form controls usually encapsulate a control written in pure JS, such as jQuery UI’s Slider. In this article I’ll use native formControl terms to distinguish Angular specific formControl from the form controls you use in HTML, but you need to know that any custom formControl can interact with a formControl directive, not a native formControl such as input.

The number of native form controls is limited, but custom form controls are infinite, so Angular needs a generic mechanism to bridge native/custom form controls with formControl directives, and that’s what ControlValueAccessor does. This object Bridges the native formControl with the formControl directive and synchronizes the values of both. Here’s how the official document describes it:

A ControlValueAccessor acts as a bridge between the Angular forms API and a native element in the DOM.

Any component or directive can be converted to an object of type ControlValueAccessor by implementing the ControlValueAccessor interface and registering it as NG_VALUE_ACCESSOR. We’ll see how later. In addition, this interface defines two important methods, writeValue and registerOnChange:

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  ...
}
Copy the code

The formControl directive uses the writeValue method to set the value of the native formControl. Use the registerOnChange method to register callback functions that are triggered each time the value of a native form control is updated. You might refer to these three lines, L186 and L43, and L85. You need to pass the updated value to the callback so that the corresponding Angular form control value is updated. See how Angular writes DefaultValueAccessor, L52 and L89, to pass each updated value of the input control to the callback function. Use the registerOnTouched method to register callbacks that are triggered when a user interacts with a control.

Here’s how Angular form controls interact with native form controls using ControlValueAccessor. FormControl and you write or presents to provide CustomControlValueAccessor both are bound to the native DOM element’s instructions, And formControl instructions needed a CustomControlValueAccessor/component, to exchange data with native DOM element. :

Again, ControlValueAccessor always interacts with Angular form controls whether they are explicitly created using reactive forms or implicitly created using template-driven forms.

Angular also creates Angular form controls for all native DOM form elements:

Accessor Form Element
DefaultValueAccessor input,textarea
CheckboxControlValueAccessor input[type=checkbox]
NumberValueAccessor input[type=number]
RadioControlValueAccessor input[type=radio]
RangeValueAccessor input[type=range]
SelectControlValueAccessor select
SelectMultipleControlValueAccessor select[multiple]

As you can see from the table above, Angular uses the DefaultValueAccessor directive when it encounters an input or Textarea DOM native control in a component template:

@Component({
  selector: 'my-app',
  template: `  `
})
export class AppComponent {
  ctrl = new FormControl(3);
}
Copy the code

All form directives, including the formControl directive in the code above, call the setUpControl function to allow the formControl to interact with DefaultValueAccessor. When the formControl directive itself is instantiated, it calls setUpControl() to install the DefaultValueAccessor directive that is also bound to the input, such as L85. The formControl directive can then exchange data with the input element using DefaultValueAccessor. See the formControl directive for details:

export classFormControlDirective ... {... ngOnChanges(changes: SimpleChanges):void {
    if (this._isControlChanged(changes)) {
      setUpControl(this.form, this);
Copy the code

The source of the setUpControl function also shows how native form controls and Angular form controls synchronize data:

export function setUpControl(control: FormControl, dir: NgControl) {
  
  // initialize a form control
  // Call writeValue() to initialize the form control value
  dir.valueAccessor.writeValue(control.value);
  
  // setup a listener for changes on the native control
  // and set this value to form control
  // Set the listener when the native control value is updated. Whenever the native control value is updated, the Angular form control value is updated
  valueAccessor.registerOnChange((newValue: any) = > {
    control.setValue(newValue, {emitModelToViewChange: false});
  });

  // setup a listener for changes on the Angular formControl
  // and set this value to the native control
  // Set the Angular form control value update listener. Whenever an Angular form control value is updated, the native control value is updated
  control.registerOnChange((newValue: any.) = > {
    dir.valueAccessor.writeValue(newValue);
  });
Copy the code

Once we understand the internals, we can implement our custom Angular form controls.

Component wrapper

Because Angular provides a control-value accessor for all default native controls, you need to write a new control-value accessor when encapsulating a third-party plug-in or component. We’ll use the jQuery UI library slider plugin mentioned above to implement a custom form control.

Simple wrapper

The basic implementation is simply wrapped to display on the screen, so we need an NgxJquerySliderComponent and render slider in its template:

@Component({
  selector: 'ngx-jquery-slider',
  template: ` 
      
`
, styles: ['div {width: 100px}']})export class NgxJquerySliderComponent { @ViewChild('location') location; widget; ngOnInit() { this.widget = $(this.location.nativeElement).slider(); }}Copy the code

Here we create a Slider control on a native DOM element using the standard jQuery method, and then reference the control using the Widget property.

Once the slider component is simply wrapped, we can use it in the parent template:

@Component({
  selector: 'my-app',
  template: ` 

Hello {{name}}

`
}) export class AppComponent { ... } Copy the code

To run the program we need to add jQuery dependencies. For simplicity, add global dependencies to index.html:

<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<link rel="stylesheet" href="/ / code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
Copy the code

Here is the source code for installing dependencies.

Interactive form controls

The above implementation does not allow our custom slider to interact with the parent, so we have to use input/output bindings for data communication between components:

export class NgxJquerySliderComponent {
  @ViewChild('location') location;
  @Input() value;
  @Output(a)private valueChange = new EventEmitter();
  widget;

  ngOnInit() {
    this.widget = $(this.location.nativeElement).slider();   
    this.widget.slider('value'.this.value);
    this.widget.on('slidestop'.(event, ui) = > {
      this.valueChange.emit(ui.value);
    });
  }

  ngOnChanges() {
    if (this.widget && this.widget.slider('value')! = =this.value) {
      this.widget.slider('value'.this.value); }}}Copy the code

Once the slider component is created, you can subscribe to the SlideStop event to get the changed value, and once the SlideStop event is fired, you can notify the parent using the output event emitter valueChanges. We can also use the ngOnChanges lifecycle hook to track changes in the value of the input property and set that value to the value of the Slider whenever it changes.

Then there is the code implementation of how to use slider in the parent component:

<ngx-jquery-slider
    [value]="sliderValue"
    (valueChange)="onSliderValueChange($event)">
</ngx-jquery-slider>
Copy the code

The source code is here.

However, if we want to use the Slider component as part of the form and communicate with its data using template-driven or reactive form instructions, we need to implement the ControlValueAccessor interface. Since we’re implementing a new way of communicating components, we don’t need a standard way of binding input and output properties, so remove the code. (Note: The standard input/output attribute binding communication method was first implemented and then removed, mainly to introduce a new form component interaction method, namely ControlValueAccessor.)

Implement custom control value accessor

Implementing a custom control value accessor is not difficult and requires only two steps:

  1. registeredNG_VALUE_ACCESSORThe provider
  2. implementationControlValueAccessorinterface

The NG_VALUE_ACCESSOR provider specifies the class that implements the ControlValueAccessor interface and is used by Angular to synchronize with formControl, usually using a component class or directive. All form directives use the NG_VALUE_ACCESSOR flag to inject the control value accessor, and then select the appropriate accessor. Either select DefaultValueAccessor or a built-in data accessor, otherwise Angular selects a custom data accessor and has only one custom data accessor.

Let’s first define the provider:

@Component({
  selector: 'ngx-jquery-slider',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: NgxJquerySliderComponent,
    multi: true
  }]
  ...
})
class NgxJquerySliderComponent implements ControlValueAccessor {...}
Copy the code

We specify the class name directly in the component decorator, whereas the Angular source code is implemented outside the class decorator by default:

export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef((a)= > DefaultValueAccessor),
  multi: true
};
@Directive({
  selector:'input',
  providers: [DEFAULT_VALUE_ACCESSOR]
  ...
})
export class DefaultValueAccessor implements ControlValueAccessor {}
Copy the code

The forwardRef (What is forwardRef in Angular and why we need it) needs to be used outside the forwardRef. When implementing custom ControlValueAccessors, I recommend putting them in the class decorator.

Once the provider is defined, let’s implement the controlValueAccessor interface:

export class NgxJquerySliderComponent implements ControlValueAccessor {
  @ViewChild('location') location;
  widget;
  onChange;
  value;
  
ngOnInit() {
	this.widget = $(this.location.nativeElement).slider(this.value);
   this.widget.on('slidestop'.(event, ui) = > {
      this.onChange(ui.value);
    });
}
  
writeValue(value) {
    this.value = value;
    if (this.widget && value) {
      this.widget.slider('value', value);
    }
  }
  
registerOnChange(fn) { this.onChange = fn;  }

registerOnTouched(fn) {  }
Copy the code

Since we are not interested in whether the user interacts with the component, leave registerOnTouched empty for now. In registerOnChange we simply save a reference to the fn callback passed in by the formControl directive, which is triggered every time the slider component changes in value. Within the writeValue method we pass the resulting value to the slider component.

Now let’s make an interactive diagram of the features described above:

If you compare the simple wrapper to the controlValueAccessor wrapper, you’ll see that parent-child components interact differently, even though the wrapped component interacts with the Slider component the same way. You may have noticed that the formControl directive actually simplifies the way you interact with the parent component. Here we use writeValue to write data to child components, while ngOnChanges are used in the simple wrapper method; Call the this.onchange method to output data, whereas use this.Valuechange. Emit (uI.value) in the simple wrapper method.

Now, the complete code for the custom Slider form control that implements the ControlValueAccessor interface is as follows:

@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <span>Current slider value: {{ctrl.value}}</span>
      <ngx-jquery-slider [formControl]="ctrl"></ngx-jquery-slider>
      <input [value]="ctrl.value" (change)="updateSlider($event)"> `})export class AppComponent {
  ctrl = new FormControl(11);

  updateSlider($event) {
    this.ctrl.setValue($event.currentTarget.value, {emitModelToViewChange: true}); }}Copy the code

You can see the final implementation of the program.

Github

Github repository for the project.