🅰️ Angular Q46 / 125

How does Angular form validation work?

AI-Powered Answer ✓ Answered

Angular provides a robust and flexible system for validating user input in forms, supporting both template-driven forms and reactive forms. It allows you to ensure data integrity and provide a good user experience by giving immediate feedback on input validity.

Two Approaches: Template-Driven vs. Reactive Forms

Angular offers two distinct approaches for building forms: template-driven forms and reactive forms. While both support the same validation capabilities, their implementation differs significantly.

Template-Driven Forms Validation

In template-driven forms, validation logic is primarily defined directly within the HTML template using directives. Angular automatically creates form control instances behind the scenes based on these directives.

  • ngModel directive: Binds form controls to data properties.
  • HTML5 validation attributes: required, minlength, maxlength, pattern, email.
  • Angular-specific directives: Though often HTML5 attributes suffice, Angular provides equivalents like ng-required.
  • #fieldName="ngModel": Exports the NgModel directive into a local template variable, allowing access to its properties (e.g., valid, invalid, dirty, touched).
html
<form #heroForm="ngForm">
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" id="name" name="name"
           class="form-control"
           required minlength="4" #name="ngModel"
           [(ngModel)]="model.name">
    <div *ngIf="name.invalid && (name.dirty || name.touched)"
         class="alert alert-danger">
      <div *ngIf="name.errors?.required">
        Name is required.
      </div>
      <div *ngIf="name.errors?.minlength">
        Name must be at least 4 characters long.
      </div>
    </div>
  </div>
  <button type="submit" [disabled]="!heroForm.valid">Submit</button>
</form>

Reactive Forms Validation

Reactive forms provide a more explicit and programmatic way to manage form validation. Validation logic is defined directly in the component class using FormControl, FormGroup, and FormArray instances.

  • FormControl: Represents a single input field.
  • FormGroup: Represents a collection of FormControl instances.
  • Validators class: Provides a set of built-in validation functions (e.g., Validators.required, Validators.minLength, Validators.pattern).
  • Custom validators: Functions that return a validation error object or null.
  • Asynchronous validators: For server-side validation or operations with a delay.
typescript
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-hero-form-reactive',
  templateUrl: './hero-form-reactive.component.html',
  styleUrls: ['./hero-form-reactive.component.css']
})
export class HeroFormReactiveComponent implements OnInit {
  heroForm!: FormGroup;

  ngOnInit(): void {
    this.heroForm = new FormGroup({
      'name': new FormControl(null, [
        Validators.required,
        Validators.minLength(4)
      ]),
      'email': new FormControl(null, [
        Validators.required,
        Validators.email
      ])
    });
  }

  onSubmit() {
    console.log(this.heroForm.value);
  }
}
html
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" id="name" formControlName="name" class="form-control">
    <div *ngIf="heroForm.get('name')?.invalid && (heroForm.get('name')?.dirty || heroForm.get('name')?.touched)"
         class="alert alert-danger">
      <div *ngIf="heroForm.get('name')?.errors?.required">
        Name is required.
      </div>
      <div *ngIf="heroForm.get('name')?.errors?.minlength">
        Name must be at least 4 characters long.
      </div>
    </div>
  </div>
  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" id="email" formControlName="email" class="form-control">
    <div *ngIf="heroForm.get('email')?.invalid && (heroForm.get('email')?.dirty || heroForm.get('email')?.touched)"
         class="alert alert-danger">
      <div *ngIf="heroForm.get('email')?.errors?.required">
        Email is required.
      </div>
      <div *ngIf="heroForm.get('email')?.errors?.email">
        Please enter a valid email.
      </div>
    </div>
  </div>
  <button type="submit" [disabled]="!heroForm.valid">Submit</button>
</form>

Validator States and CSS Classes

Angular automatically applies CSS classes to form controls based on their validation state. This allows for easy styling to provide visual feedback to users.

Class NameDescription
`ng-valid`Control's value is valid
`ng-invalid`Control's value is invalid
`ng-pristine`Control's value has not changed
`ng-dirty`Control's value has changed
`ng-untouched`Control has not been visited
`ng-touched`Control has been visited

These classes can be used in your CSS to style valid/invalid inputs, for example:

css
.ng-valid[required], .ng-valid.required {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
}

Custom Validators

When built-in validators are not sufficient, you can create custom validator functions. These functions take a FormControl or AbstractControl as an argument and return a validation error object if invalid, or null if valid.

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

export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {forbiddenName: {value: control.value}} : null;
  };
}

// Usage in reactive forms:
// new FormControl(null, [forbiddenNameValidator(/bob/i)])

Asynchronous Validators

For validation that requires a server request or a delay, asynchronous validators are used. These validators return a Promise<ValidationErrors | null> or an Observable<ValidationErrors | null>.

typescript
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

export function uniqueUsernameValidator(userService: any): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
    return timer(500).pipe( // Simulate network delay
      switchMap(() => userService.checkUsernameNotTaken(control.value)),
      map(isTaken => (isTaken ? { uniqueUsername: true } : null))
    );
  };
}

// Usage in reactive forms:
// new FormControl(null, [], [uniqueUsernameValidator(this.userService)])