Reusable custom form control components in Angular
Introduction
While developing in angular, it is always imperative to follow the principle of "Do not repeat yourself". Components can sometimes be really heavy and complex. When I say complex, I mean on both functional (TS) and design (HTML and CSS). Sometimes a reusable component has to be decomposed into smaller component chunks to keep everything clean. That's why angular's approach in using components excels since it makes it easy to find the part of the code to modify. One of the most powerful features in Angular is Reactive forms. Reactive forms makes it easy to develop really complex forms by making sure the state of the form is fully synchronized with the UI. So you don't have to create too many bindings between the component and its caller. Our goal here is to demonstrate how to create custom components compatible with angular's reactive forms feature.
NB: Before reading this document, you should have some knowledge about Angular (17+), Standalone components, Angular signals and Angular Reactive Forms.
Implementation
Create the form group
private readonly fb = inject(FormBuilder);
form = this.fb.group({
email: [''],
password: [''],
});
Use the form group inside the html template
As you can see, I am using formControlName to pass the control to the component
<form [formGroup]="form">
<krk-text-field formControlName="email" placeholder="Email" />
<krk-text-field formControlName="password" placeholder="Password" />
</form>
Create the reusable component (text-field)
领英推荐
Step 1: provide NG_VALUE_ACCESSOR
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextFieldComponent),
multi: true,
},
],
Step 2: Implement ControlValueAccessor to the class
export class TextFieldComponent implements ControlValueAccessor {
Step 3: Understand what each function does
text-field.component.html
<div class="krk-text-field-wrapper" [class.krk-filled]="!!value()">
<input [value]="value()" (input)="onValueChange($event)" />
<span class="krk-placeholder" *ngIf="placeholder">{{ placeholder() }}</span>
<span class="krk-error">{{error()}}</span>
</div>
text-field.component.ts
import { Component, forwardRef, input, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
type FieldType = string | null;
@Component({
selector: 'krk-text-field',
imports: [CommonModule],
templateUrl: './text-field.component.html',
styleUrl: './text-field.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextFieldComponent),
multi: true,
},
],
})
export class TextFieldComponent implements ControlValueAccessor {
placeholder = input('');
error = input('');
value = signal<FieldType>('');
disabled = signal(false);
onChange!: (value: FieldType) => void;
onTouched!: () => void;
// triggered when calling control.setValue("Hello World")
writeValue(value: FieldType): void {
this.value.set(value);
}
registerOnChange(fn: (value: FieldType) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
// Triggered when calling control.enable() or control.disable()
setDisabledState?(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
// Called from the html template of this component.
onValueChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.value.set(value);
// Notify the control that changes have been made.
this.onChange(value);
this.onTouched();
}
}
Software Engineer at inmind .ai
1 个月Useful tips, keep it up ?? ??
Software engineer | Diebold Nixdorf Solution
1 个月Great advice
Full Stack Web Developer
1 个月Great stuff! Keep them coming
Software Developer
1 个月Thanks for sharing
Consultant & Pre-Sales Engineer | Business Development | B2B | Swift Financial Messaging | Swift Certified Assessor | Cybersecurity Engineer | GRC
1 个月Great advice! Very helpful ????