Angular Reactive Forms: Building Custom Form Controls

Angular’s reactive forms module enables us to build and manage complexed forms in our application using a simple but powerful model. You can build your own custom form controls that work seamlessly with the Reactive Forms API. The core idea behind form controls is the ability to access a control’s value. This is done with a set of directives that implement the ControlValueAccessor interface.

The ControlValueAccessor Interface

ControlValueAccessor is an interface for communication between a FormControl and the native element. It abstracts the operations of writing a value and listening for changes in the DOM element representing an input control. The following snippet was taken from the Angular source code, along with the original comments:

The ControlValueAccessor interface.

interface ControlValueAccessor {
 /**   
 * Write a new value to the element.
 */ 

writeValue(obj: any): void;
 /**
 * Set the function to be called when the control receives a change event.
 */ 

registerOnChange(fn: any): void;
 /**    
 * Set the function to be called when the control receives a touch event.
 */ 

registerOnTouched(fn: any): void;
 /**    
 * This function is called when the control status changes to or from "DISABLED".
 * Depending on the value, it will enable or disable the appropriate DOM element.
 * @param isDisabled
 */

setDisabledState?(isDisabled: boolean): void;
} 

 

ControlValueAccessor Directives

Each time you use the formControl or formControlName directive on a native <input> element, one of the following directives is instantiated, depending on the type of the input:

  1. DefaultValueAccessor – Deals with all input types, excluding checkboxes, radio buttons, and select elements.
  2. CheckboxControlValueAccessor – Deals with checkbox input elements.
  3. RadioControlValueAccessor – Deals with radio control elements [RH: Or just “radio buttons”
    or “radio button inputs”?].
  4. SelectControlValueAccessor – Deals with a single select element.
  5. SelectMultipleControlValueAccessor – Deals with multiple select elements.

 

Become part of our International JavaScript Community now!
LEARN MORE ABOUT iJS:

 

Let’s peek under the hood of the CheckboxControlValueAccessor directive to see how it implements the ControlValueAccessor interface. The following snippet was taken from the Angular docs:

checkbox_value_accessor.ts.

import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; 

export const CHECKBOX_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxControlValueAccessor),
multi: true, 
}; 

@Directive({
selector : `input[type=checkbox][formControlName], 
              input[type=checkbox][formControl],
              input[type=checkbox][ngModel]`,
  host : {
    '(change)': 'onChange($event.target.checked)',
    '(blur)'  : 'onTouched()'
  }
  ,
  providers: [CHECKBOX_VALUE_ACCESSOR]
}) 

export class CheckboxControlValueAccessor implements ControlValueAccessor {
onChange = (_: any) => {};
onTouched = () => {}; 
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}

writeValue(value: any): void {
 
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', value);
} 

registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn; 
}
 
registerOnTouched(fn: () => {}): void {
this.onTouched = fn; 
} 

setDisabledState(isDisabled: boolean): void { 
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled',  isDisabled);
  }
}

 

Let’s explain what’s going on:

  1. This directive is instantiated when an input of type checkbox is declared with the formControl, formControlName, or ngModel directives.
  2. The directive listens to change and blur events in the host.
  3. This directive will change both the checked and disabled properties of the element, so the
    ElementRef and Renderer [RH: ElementRef and Renderer what? Classes?] are injected.
  4. The writeValue() implementation is straight forward: it sets the checked property of the
    native element. Similarly, setDisabledState() sets the disabled property.
  5. The function being passed to the registerOnChange() method is responsible for updating the outside world about changes to the value. It is called in response to a change event with the input value.
  6. The function being passed to the registerOnTouched() method is triggered by the blur event.
  7. Finally, the CheckboxControlValueAccessor directive is registered as a provider.

 

Sample Custom Form Control: Button Group

Let’s build a custom FormControl based on the Twitter Bootstrap button group component.
We will start with a simple component:

custom-control.component.ts.

import {Component} from "@angular/core"; 

@Component({
selector : 'rf-custom-control',
templateUrl: 'custom-control.component.html',
 })
export class CustomControlComponent { 

private level: string;
private disabled: boolean; 

constructor(){
this.disabled = false;
}
 
public isActive(value: string): boolean {
return value === this.level; 
} 

public setLevel(value: string): void {
this.level = value; 
}
} 

 

Here is the template:

custom-control.component.html.

<div class="btn-group btn-group-lg"> 

<button type="button"
class="btn btn-secondary"
[class.active]="isActive('low')"
[disabled]="disabled"
(click)="setLevel('low')">low</button> 

<button type="button"
class="btn btn-secondary"
[class.active]="isActive('medium')"
[disabled]="disabled"
(click)="setLevel('medium')">medium</button>

<button type="button"
class="btn btn-secondary"
[class.active]="isActive('high')"
[disabled]="disabled" (click)="setLevel('high')">high</button> 
</div>

 

Next, let’s implement the ControlValueAccessor interface:

custom-control.ts component class.

export class CustomControlComponent implements ControlValueAccessor {

private level: string;
private disabled: boolean;
private onChange: Function; 
private onTouched: Function; 

constructor() {
this.onChange = (_: any) => {};
this.onTouched = () => {};
this.disabled = false; 
} 

public isActive(value: string): boolean {
return value === this.level; 
} 

public setLevel(value: string): void {
this.level = value;
this.onChange(this.level);
this.onTouched(); 
} 

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

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

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

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; 
}
} 

 

The last step is to register our custom control component under the NG_VALUE_ACCESSOR token. NG_VALUE_ACCESSOR is an OpaqueToken used to register multiple ControlValue providers. (If you are not familiar with OpaqueToken, the multi property, and the forwardRef() function, read the official dependency injection guide on the Angular website.)

Here’s how we register the CustomControlComponent as a provider:

Registering the control as a provider.

const CUSTOM_VALUE_ACCESSOR: any = {
provide : NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomControlComponent),
multi : true, 
}; 


@Component({
selector : 'app-custom-control',
providers : [CUSTOM_VALUE_ACCESSOR],
templateUrl: 'custom-control.component.html', 
}) 

 

Our custom control is ready. Let’s try it out:

app.component.ts.

import {Component, OnInit} from "@angular/core";
import {FormControl} from "@angular/forms"; 

@Component({
selector: 'rf-root',
template: ` 
<div class="container">
<h1 class="h1">REACTIVE FORMS</h1> 
 
      <rf-custom-control [formControl]="buttonGroup"></rf-custom-control>

<pre>
<code> 
          Control dirty:   {{buttonGroup.dirty}}
          Control touched: {{buttonGroup.touched}}
        </code>
</pre> 
</div>
`, 
})
export class AppComponent implements OnInit {

public buttonGroup: FormControl; 

constructor() {
this.buttonGroup = new FormControl('medium'); 
} 

ngOnInit(): void {
this.buttonGroup.valueChanges.subscribe(value => console.log(value)); 
} 
} 

 

This tutorial is an excerpt from iJS speaker Nir Kaufman’s eBook “Angular Reactive Forms – A comprehensive guide for building forms with Angular”. The complete book can be purchased in the Leanpub store: https://leanpub.com/angular-forms

Interview with Nir Kaufman

Top Articles About Angular

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows

DON'T MISS ANY NEWS