Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Angular

Angular Custom Form Controls with Reactive Forms and NgModel

Cory Rylan

- 6 minutes

Updated

This article has been updated to the latest version Angular 17 and tested with Angular 16. The content is likely still applicable for all Angular 2 + versions.

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!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Creating Dynamic Tables in Angular

Learn how to easily create HTML tables in Angular from dynamic data sources.

Read Article
Web Components

Reusable Component Patterns - Default Slots

Learn about how to use default slots in Web Components for a more flexible API design.

Read Article
Web Components

Reusable Component Anti-Patterns - Semantic Obfuscation

Learn about UI Component API design and one of the common anti-patterns, Semantic Obfuscation.

Read Article