How does Angular form validation work?
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 theNgModeldirective into a local template variable, allowing access to its properties (e.g.,valid,invalid,dirty,touched).
<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 ofFormControlinstances.Validatorsclass: 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.
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);
}
}
<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 Name | Description |
|---|---|
| `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:
.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.
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>.
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)])