import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";

// Custom Validator to check the length of the username
export function usernameValidator(minLength: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && value.length < minLength) {
      return {
        usernameTooShort: {
          requiredLength: minLength,
          actualLength: value.length,
        },
      };
    }
    return null;
  };
}

Understanding Custom Validators

Angular's Reactive Forms provide a comprehensive system for handling user input, validation, and interaction. While the framework ships with standard validators such as Validators.required and Validators.email, real-world applications often demand validation logic that goes beyond these built-in options. This becomes especially relevant when dealing with custom form controls built on ControlValueAccessor.

The Value of Custom Validators for Custom Controls

Integrating validation directly into custom form controls that implement ControlValueAccessor ensures they operate consistently within the broader form structure. Failing to implement proper validation can leave your application vulnerable to invalid or unsafe data being accepted and processe, undermining both security and data quality.

Building a Basic Custom Validator

Consider a scenario where you have a custom control designed for username entry. A common requirement is enforcing a minimum length—for instance, usernames must be at least five characters long. The validator function below accomplishes this:

The usernameValidator returns a ValidatorFn that inspects the control's value. If the value falls short of the length requirement, it produces an error object; otherwise, it responds with null.

Incorporating the Validator into a Reactive Form

Here's how you would attach this validator to a form control within a Reactive Form that utilises a custom component based on ControlValueAccessor:

import { Component } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { usernameValidator } from "./validators/username-validator";

@Component({
  selector: "app-root",
  template: `
    <form [formGroup]="form">
      <app-custom-input formControlName="username"></app-custom-input>
      <div *ngIf="form.get('username')?.errors?.usernameTooShort">
        Username must be at least
        {{ form.get("username")?.errors?.usernameTooShort.requiredLength }}
        characters long.
      </div>
    </form>
  `,
})
export class AppComponent {
  form = new FormGroup({
    username: new FormControl("", [usernameValidator(5)]),
  });
}

In the snippet above, the username control is configured with usernameValidator, enforcing the length constraint. When validation fails, the template conditionally shows an appropriate error message to the user.

Wiring Validators into ControlValueAccessor

For validation to work seamlessly with your custom control, it's crucial that the control interacts with Angular's validation pipeline correctly. In typical setups, Angular handles this automatically when the control is part of a form group. However, for controls with intricate internal logic, you may need to initiate validation manually.

The following adjustment to CustomInputComponent demonstrates how to achieve this:

import { Component, forwardRef, Input } from "@angular/core";
import {
  ControlValueAccessor,
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS,
  Validator,
  AbstractControl,
  ValidationErrors,
} from "@angular/forms";

@Component({
  selector: "app-custom-input",
  template: `<input [disabled]="isDisabled" (input)="onInput($event)" />`,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    },
  ],
})
export class CustomInputComponent implements ControlValueAccessor, Validator {
  @Input() isDisabled = false;
  private value: string = "";

  private onChange: (value: any) => void;
  private onTouched: () => void;

  writeValue(value: any): void {
    this.value = value;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  onInput(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.value = input.value;
    this.onChange(this.value);
  }

  validate(control: AbstractControl): ValidationErrors | null {
    return control.value ? null : { required: true };
  }
}

This version of the component adopts the Validator interface, thereby supplying its own validation routine. The validate method assesses the control's value and returns an error indication when the value doesn't meet the criteria.

Displaying Validation Feedback

To communicate validation failures to the user, you can dynamically display error messages in the template, as demonstrated with the usernameTooShort error. This pattern can be broadened to accommodate an array of different validators, each with its own specific message.

Stacking Multiple Validators

Combining several validation rules for a single control is straightforward. Let's say you also want to prohibit usernames from using certain restricted keywords, in addition to enforcing the minimum length:

export function restrictedUsernameValidator(
  restrictedNames: string[]
): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && restrictedNames.includes(value)) {
      return { usernameRestricted: { value } };
    }
    return null;
  };
}

const combinedValidators = [
  usernameValidator(5),
  restrictedUsernameValidator(["admin", "root"]),
];

You can then register both validators against the form control like so:

form = new FormGroup({
  username: new FormControl("", combinedValidators),
});

Adapting Validation on the Fly

There are scenarios where validation requirements shift based on the state of other form fields or user actions. To handle this, you can dynamically update the validators assigned to a control:

this.form.get("username")?.setValidators([usernameValidator(8)]);
this.form.get("username")?.updateValueAndValidity();

This strategy gives you the flexibility to modify validation behavior on demand, keeping the form accurate and reliable under changing circumstances.

Final Thoughts

By incorporating custom validators into your ControlValueAccessor-based controls, you guarantee that your bespoke form elements are both flexible and dependable. Adopting these techniques ensures your components integrate smoothly with Angular's Reactive Forms, delivering a seamless and consistent experience across your entire application.

Leveraging custom validators not only fulfills your application's specific validation needs but also upholds rigorous standards for data integrity and input quality.