Angular is a TypeScript-based open-source web application framework led by the Angular Team at Google and a community of individuals and corporations. In this article, we’ll explore how to create a custom control in Angular that can be used as a reactive form control. Angular provides extensive extensibility features to enhance the behavior of the framework.
To create a custom control, we need to implement the ControlValueAccessor
interface, which includes the following four methods:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
For this article, we’ll create a custom spinner control. Let’s start by creating a new Angular application using the following command:
ecuting the following command
ng new app_name
Next, create a component called SpinnerControl
and paste the following code. The code is self-explanatory and creates the custom spinner control.
import { Component, forwardRef, Input } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
@Component({
selector: "app-number",
template: `
<div class="container">
<div class="left">
<input
class="number-input"
[value]="value"
type="number"
[disabled]="disabled"
/>
</div>
<div class="right">
<button (click)="onIncrement()">+</button>
<button (click)="onDeincrement()">-</button>
</div>
</div>
`,
styles: [
`
.container {
display: flex;
padding: 0px;
}
.right {
display: flex;
flex-direction: column;
}
.number-input {
height: 40px;
}
button {
height: 20px;
width: 20px;
border: 0px;
background-color: #aa33dd;
}
`
]
})
export class SpinnerControl implements ControlValueAccessor {
@Input()
public value: number;
@Input()
public disabled: boolean;
onChanged: any = () => {};
onTouched: any = () => {};
writeValue(val): void {
console.log(val);
this.value = +val || 0;
}
registerOnChange(fn: any) {
this.onChanged = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
numberChange($event) {
this.onTouched();
this.onChanged($event.target.value);
this.setDisabledState(this.disabled);
}
onIncrement() {
this.value = this.value + 1;
this.onChanged(this.value);
}
onDeincrement() {
this.value = this.value - 1;
this.onChanged(this.value);
}
}
Method Name | Description |
---|---|
writeValue |
Write a new value to the element |
registerOnChange |
Is called when the control values change in the UI |
registerOnTouched |
Is called by the forms API to update the form model on blur |
In the above code, we’ve created the SpinnerControl
component that implements the ControlValueAccessor
interface. It includes the necessary methods to handle the control’s value, changes, and disabled state. The template defines the UI elements of the spinner control.
How to use?
Before using this custom reactive form control, you have to register this with angular providers array. See the below code for registration
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SpinnerControl),
multi: true
}
]
NG_VALUE_ACCESSOR
:NG_VALUE_ACCESSOR
is a predefined Angular token that represents the accessor for writing and reading the value of a control. It is used to connect the custom control to the Angular forms API.forwardRef
:forwardRef
is a function that is used to refer to a class that is not yet defined. In this case, it allows us to reference theSpinnerControl
class before it is fully defined.multi
:multi
is a boolean flag that specifies whether the provider is a multi-provider or a single-provider. In this case, we set it totrue
to indicate that there can be multiple providers forNG_VALUE_ACCESSOR
.
By including the NG_VALUE_ACCESSOR
provider in the providers
array, we register the SpinnerControl
as a valid control that can be used within Angular’s reactive forms.
Final code looks like below.
import { Component, forwardRef, Input } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
@Component({
selector: "app-number",
template: `
<div class="container">
<div class="left">
<input
class="number-input"
[value]="value"
type="number"
[disabled]="disabled"
/>
</div>
<div class="right">
<button (click)="onIncrement()">+</button>
<button (click)="onDeincrement()">-</button>
</div>
</div>
`,
styles: [
`
.container {
display: flex;
padding: 0px;
}
.right {
display: flex;
flex-direction: column;
}
.number-input {
height: 40px;
}
button {
height: 20px;
width: 20px;
border: 0px;
background-color: #aa33dd;
}
`
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SpinnerControl),
multi: true
}
]
})
export class SpinnerControl implements ControlValueAccessor {
@Input()
public value: number;
@Input()
public disabled: boolean;
onChanged: any = () => {};
onTouched: any = () => {};
writeValue(val): void {
console.log(val);
this.value = +val || 0;
}
registerOnChange(fn: any) {
this.onChanged = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
numberChange($event) {
this.onTouched();
this.onChanged($event.target.value);
this.setDisabledState(this.disabled);
}
onIncrement() {
this.value = this.value + 1;
this.onChanged(this.value);
}
onDeincrement() {
this.value = this.value - 1;
this.onChanged(this.value);
}
}
To use the custom control, you need to register it with the Angular providers array in the component where you want to use it. Here’s an example of how to register the SpinnerControl
:
import { Component } from "@angular/core";
@Component({
selector: "app-my-component",
template: `
<form [formGroup]="myForm">
<app-spinner formControlName="spinnerControl"></app-spinner>
</form>
`
})
export class MyComponent {
// ...
}
In the above code, we’re using the app-spinner
component as a form control by binding it to the spinnerControl
form control name.
That’s it! You’ve successfully created a custom spinner control in Angular that can be used as a reactive form control. The control implements the necessary ControlValueAccessor
methods to integrate with Angular’s forms API.
Further reading
What is forwardRef
From Angular’s API docs on forwardRef
:
forwardRef
is used when the token which we need to refer to for the purposes of DI is declared, but not yet defined. It is also used when the token which we use when creating a query is not yet defined.