How do you implement custom form controls?
Custom form controls in Angular allow you to integrate your own UI components seamlessly with Angular's reactive or template-driven forms, making them behave like native input elements. This is achieved by making your component act as a `ControlValueAccessor`.
Core Concept: ControlValueAccessor
To make a custom component compatible with Angular forms, it must implement the ControlValueAccessor interface. This interface acts as a bridge, allowing Angular's FormControl (or ngModel) to read and write values to your custom component, and to register callbacks for changes and touch events.
Steps to Implement a Custom Control
1. Create Your Component
Start with a standard Angular component. This component will encapsulate your custom UI and logic.
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
export class CustomInputComponent implements ControlValueAccessor {
// Component properties and methods will go here
}
2. Provide NG_VALUE_ACCESSOR
Register your component as a ControlValueAccessor using the NG_VALUE_ACCESSOR token in the component's providers array. useExisting refers to the component class itself, and multi: true is crucial because there can be multiple ControlValueAccessor implementations in an application (though only one per component).
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
3. Implement ControlValueAccessor Methods
The ControlValueAccessor interface requires four methods that facilitate communication between the form control and your component:
writeValue(obj: any): Called by Angular forms to write a value to the component. Use this to update your component's internal state or UI.registerOnChange(fn: any): Registers a callback function (fn) that Angular will call when the control's value changes (i.e., when your custom component wants to notify the form).registerOnTouched(fn: any): Registers a callback function (fn) that Angular will call when the control has been touched (i.e., when your custom component receives a blur event).setDisabledState?(isDisabled: boolean): (Optional) Called by Angular to enable or disable the control. Use this to update your component's UI to reflect its disabled state.
export class CustomInputComponent implements ControlValueAccessor {
@Input() label: string = ''; // Example input for the label
value: any = ''; // Internal value representing the form control's value
onChange: any = () => {}; // Callback to notify Angular of value changes
onTouched: any = () => {}; // Callback to notify Angular of 'touched' state
isDisabled: boolean = false; // Internal state for disabled
// Called by the forms API to write to the view
writeValue(obj: any): void {
this.value = obj;
}
// Called by the forms API to register a callback function
// that should be called when the control's value changes in the UI
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Called by the forms API to register a callback function
// that should be called when the control receives a blur event.
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// Called by the forms API to enable or disable the control
setDisabledState?(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
// Method to emit changes from your internal UI element (e.g., an input event)
onInput(event: Event) {
const newValue = (event.target as HTMLInputElement).value;
this.value = newValue; // Update internal value
this.onChange(newValue); // Notify Angular forms of the change
}
}
4. Create the Template
Your component's template will contain the UI elements that represent your custom control. Bind to the value property and call onInput (or similar) when the internal value changes, and onTouched on blur.
<!-- custom-input.component.html -->
<div class="custom-input-container">
<label *ngIf="label">{{ label }}</label>
<input
type="text"
[value]="value"
(input)="onInput($event)"
(blur)="onTouched()"
[disabled]="isDisabled"
placeholder="Enter something..."
/>
</div>
Usage in Parent Form
Once implemented, your custom control can be used in Angular forms just like a native input, with formControlName (for reactive forms) or ngModel (for template-driven forms).
Parent Component HTML
<!-- app.component.html or parent-form.component.html -->
<form [formGroup]="myForm">
<app-custom-input label="My Custom Field" formControlName="myCustomField"></app-custom-input>
<div *ngIf="myForm.get('myCustomField')?.invalid && myForm.get('myCustomField')?.touched">
<p style="color: red;">Field is required!</p>
</div>
<p>Form Value: {{ myForm.value | json }}</p>
</form>
Parent Component TypeScript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-parent-form',
templateUrl: './parent-form.component.html'
})
export class ParentFormComponent implements OnInit {
myForm: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.myForm = this.fb.group({
myCustomField: ['', Validators.required]
});
}
}