Angular Custom Form Controls with Reactive Forms and NgModel
This post has been updated from an excerpt chapter from my new EBook Angular Form Essentials
Custom Form Controls
Custom form controls/inputs are a typical pattern in complex Angular applications. It's common to want to encapsulate HTML, CSS, and accessibility in an input component to make it easier to use in forms throughout the application. Common examples of this are datepickers, switches, dropdowns, and typeaheads. All of these types of inputs are not native to HTML. Ideally, we would like them to integrate into Angular's form system easily.
In this post, we will show how to create a switch component (app-switch
) which is essentially a checkbox with additional CSS and markup to get a physical switch like effect. This component will easily integrate into the new Angular Reactive and Template Form APIs. First, let's take a look at what our switch component will look like.
The switch component mostly mimics the behavior of a checkbox. It toggles a boolean value in our forms. In this component, we use a native checkbox and some HTML and CSS to create the switch effect. We use a particular API Angular exposes to allow us to support both Template and Reactive Form API integration. Before diving into how to build the switch component let's take a look at what it looks like when using it in our Angular application.
Custom Form Controls with Reactive Forms
Let's take a quick look at how a Reactive Form would look with our custom app-switch
component.
<h3>Reactive Forms</h3>
<form [formGroup]="myForm" (ngSubmit)="submit()">
<label for="switch-2">Switch 2</label>
<app-switch formControlName="mySwitch" id="switch-2"></app-switch>
<button>Submit</button>
</form>
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-custom-form-controls-example',
templateUrl: './custom-form-controls-example.component.html'
})
export class CustomFormControlsExampleComponent implements OnInit {
myForm: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.myForm = this.formBuilder.group({
mySwitch: [true]
});
}
submit() {
console.log(`Value: ${this.myForm.controls.mySwitch.value}`);
}
}
We can see our custom app-switch
works seamlessly with the Reactive Forms/Form Builder API just like any other text input.
Custom Form Controls with Template Forms and NgModel
NgModel allows us to bind to an input with a two-way data binding syntax similar to Angular 1.x. We can use this same syntax when using a custom form control.
<h3>NgModel</h3>
<label for="switch-1">Switch 1</label>
<app-switch [(ngModel)]="value" id="switch-1"></app-switch><br />
<strong>Value:</strong> {{value}}
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-custom-form-controls-example',
templateUrl: './custom-form-controls-example.component.html'
})
export class CustomFormControlsExampleComponent implements OnInit {
value = false;
submit() {
console.log(`Value: ${this.value}`);
}
}
Now that we see what our custom form control looks like when using with Angular's two different form APIs let's dig into the code for how the app-switch
is implemented.
Building a Custom Form Control
First, let's take a look at the template for our custom form control app-switch
.
<div
(click)="switch()"
class="switch"
[ngClass]="{ 'checked': value }"
[attr.title]="label"
>
<input
type="checkbox"
class="switch-input"
[value]="value"
[attr.checked]="value"
[id]="ID"
/>
<span class="switch-label" data-on="On" data-off="Off"></span>
<span class="switch-handle"></span>
</div>
In the component template, there are a few dynamic properties and events. There is a click event to toggle the value. We also bind to the value to set our checkbox value and our CSS class for styles.
In this post, we won't cover the CSS file for this component as it is not Angular specific, but you can dig into the source code in the included working code example below. Next, let's take a look at the app-switch
component code and dig into the API.
import { Component, Input, forwardRef, HostBinding } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SwitchComponent),
multi: true
}
]
})
export class SwitchComponent implements ControlValueAccessor {
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
@Input('value') _value = false;
onChange: any = () => {};
onTouched: any = () => {};
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
constructor() {}
registerOnChange(fn) {
this.onChange = fn;
}
writeValue(value) {
if (value) {
this.value = value;
}
}
registerOnTouched(fn) {
this.onTouched = fn;
}
switch() {
this.value = !this.value;
}
}
A lot is going on here, let's break it down. First our imports and @Component
decorator.
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SwitchComponent),
multi: true
}
]
})
The first part of our decorator is defining the component template, CSS, and selector. The API we are interested in is under providers. Under providers, we are telling the Angular DI to extend the existing NG_VALUE_ACCESSOR
token and use SwitchComponent
when requested. We then set multi to true. This mechanism enables multi providers
. Essentially allowing multiple values for a single DI token. This allows natural extensions to existing APIs for developers. This essentially registers our custom component as a custom form control for Angular to process in our templates. Next, let's look at our component class.
export class SwitchComponent implements ControlValueAccessor {
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
@Input('value') _value = false;
onChange: any = () => {};
onTouched: any = () => {};
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
writeValue(value) {
if (value) {
this.value = value;
}
}
switch() {
this.value = !this.value;
}
}
The first part of our class is the ControlValueAccessor
interface we are extending. The ControlValueAccessor
interface looks like this:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
}
We will go over the purpose of each one of these methods below. Our component takes in a couple of different @Inputs
.
export class SwitchComponent implements ControlValueAccessor {
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
...
}
The first input is the id
input. We want to be able to set an id
on the app-switch
component and pass that down to the underlying checkbox in our switch component. Doing this allows developers using our component to assign a label element to the input to get the appropriate level of accessibility. There is one trick though, we must pass the id
as an input
to add to the checkbox and then remove the id
on the app-switch
element. We need to remove the id
on the app-switch
because it is not valid to have duplicate id
attributes with the same value.
To remove the extra id
we can use the HostBinding
decorator to set the attr.id
value to null
. This allows us to receive the id
value as an input, set the checkbox id internally then delete the id
on the parent app-switch
element. Next is the @Input('input')
property.
export class SwitchComponent implements ControlValueAccessor {
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
@Input('value') _value = false;
onChange: any = () => {};
onTouched: any = () => {};
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
writeValue(value) {
if (value) {
this.value = value;
}
}
switch() {
this.value = !this.value;
}
}
The @Input('input')
allows us to take an input value named input
and map it to the _input
property. We will see the role of onChange
and onTouched
in shortly. Next, we have the following getters and setters.
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
Using getters and setters, we can set the value on the component in a private property named _value
. This allows us to call this.onChange(val)
and .onTouched()
.
The next method registerOnChange
passes in a callback function as a parameter for us to call whenever the value has changed. We set the property onChange
to the callback, so we can call it whenever our setter on the value
property is called. The registerOnTouched
method passes back a callback to call whenever the user has touched the custom control. When we call this callback, it notifies Angular to apply the appropriate CSS classes and validation logic to our custom control.
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
writeValue(value) {
if (value) {
this.value = value;
}
}
The last method to implement from the ControlValueAccessor is writeValue
. This writeValue
is called by Angular when the value of the control is set either by a parent component or form. The final method switch()
is called on the click event triggered from our switch component template.
Custom form controls are simply components that implement the ControlValueAccessor
interface. By implementing this interface, our custom controls can now work with Template and Reactive Forms APIs seamlessly providing a great developer experience to those using our components. Check out the full working demo in the link below!