DEV Community

Sonu Kapoor for This is Angular

Posted on

Custom Validators with ControlValueAccessor in Angular: Ensuring Robust Form Validations

Reactive Forms offers a robust way to manage form inputs, validations, and user interactions. In a previous article, we discussed managing the disabled property using ControlValueAccessor for custom form controls. This time, let's take it a step further by exploring how to implement custom validators for these controls to ensure your forms are not only flexible but also secure and reliable.

What Are Custom Validators?

Validators in Angular are functions that check the value of a form control and determine if it is valid or invalid based on specific criteria. Angular provides built-in validators like Validators.required and Validators.email, but there are scenarios where you need custom validation logic. This is particularly important when working with custom form controls that implement ControlValueAccessor.

Why Use Custom Validators with ControlValueAccessor?

When you create custom form controls using ControlValueAccessor, integrating validation logic ensures that these controls work seamlessly within your forms. Without proper validation, you might expose your application to potential issues, such as allowing incorrect or insecure data to be submitted.

Implementing Custom Validators

Let's start by implementing a basic custom validator. Suppose we have a custom form control for an input that accepts usernames, and we want to validate that the username is at least 5 characters long.

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

This usernameValidator function returns a ValidatorFn, which checks if the control's value meets the minimum length requirement. If it doesn't, it returns an error object; otherwise, it returns null.

Applying the Validator in a Reactive Form

Next, let's see how to apply this custom validator in a Reactive Form that includes a custom control implementing 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)]),
  });
}
Enter fullscreen mode Exit fullscreen mode

In this example, the username form control uses the usernameValidator, ensuring that the username meets the length requirement. If the validation fails, an error message is displayed.

Integrating Validators with ControlValueAccessor

To ensure that your custom control works correctly with validation, you need to make sure it calls the validation functions appropriately. Angular automatically handles this when you register your control with a form group, but if your control has complex logic, you might need to trigger validation manually.

Here's how you can update the CustomInputComponent to trigger validation:

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the CustomInputComponent implements the Validator interface, allowing it to provide its own validation logic. The validate method checks if the control's value is valid and returns an error if it isn't.

Handling Validation Errors

To display validation errors, you can conditionally render error messages in the template based on the form control's error state. This was shown earlier with the usernameTooShort error, but you can expand this to handle multiple validators.

Combining Multiple Validators

You can easily combine multiple validators for a single control. For instance, you might want to validate both the length of the username and ensure it's not a restricted keyword:

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"]),
];
Enter fullscreen mode Exit fullscreen mode

Apply these validators to the form control:

form = new FormGroup({
  username: new FormControl("", combinedValidators),
});
Enter fullscreen mode Exit fullscreen mode

Dynamic Validation

Sometimes, you need to change the validation rules based on other form values or user input. This can be achieved by updating the form control's validator dynamically:

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

This approach allows you to adjust the validation logic as needed, ensuring the form remains valid under different conditions.

Conclusion

Implementing custom validators with ControlValueAccessor in Angular ensures that your custom form controls are not only flexible but also secure and reliable. By following these best practices, you can create robust form components that seamlessly integrate with Angular’s Reactive Forms, providing a consistent and user-friendly experience across your application.

By leveraging custom validators, you ensure that your forms meet the specific needs of your application while maintaining the high standards of data integrity and user input validation.

Top comments (3)

Collapse
 
jangelodev profile image
João Angelo

Hi Sonu Kapoor,
Thanks for sharing.

Collapse
 
hakimio profile image
Tomas Rimkus

Important topic to cover could be async Angular form validators. Angular forms have pretty poor support for that.

Collapse
 
sonukapoor profile image
Sonu Kapoor • Edited

@hakimio I have an article scheduled for that as well. Keep an eye on the articles I publish.