Reusable custom form control components in Angular
Drawing created by Jean Paul Abi Ghosn

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

  • writeValue: listen to any change made from outside the component. For example by doing control.setValue("Hello World")
  • registerOnChange: Bind the change function that you will be using inside the component to notify the control for any change to update the value of the control. Note that you will have to call it yourself from inside the component.
  • registerOnTouched: Bind the onTouch function that you will be using inside the component to notify the control that the component has been touched. Note that you will have to call it yourself. This is useful if your code logic is dependent on the status of the component if it is touched or not.
  • setDisabledState: Just like writeValue, this will be triggered in case the control has been set as disabled by using control.disable().


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();
  }
}        
Charbel Abou Khalil

Software Engineer at inmind .ai

1 个月

Useful tips, keep it up ?? ??

回复
Michel Abi Akl

Software engineer | Diebold Nixdorf Solution

1 个月

Great advice

回复
Francois Matta

Full Stack Web Developer

1 个月

Great stuff! Keep them coming

回复
Marc Mansour

Software Developer

1 个月

Thanks for sharing

回复
Mohamad CHIRI

Consultant & Pre-Sales Engineer | Business Development | B2B | Swift Financial Messaging | Swift Certified Assessor | Cybersecurity Engineer | GRC

1 个月

Great advice! Very helpful ????

回复

要查看或添加评论,请登录

Jean-Paul Abi-Ghosn的更多文章

社区洞察

其他会员也浏览了