Angular Form Validation
Form Validation Overview
Angular provides a powerful validation system for both Reactive and Template-driven forms. Validators can be built-in (required, minLength, email) or custom. Angular tracks the validity state of each control and the form as a whole, making it easy to show error messages and disable submit buttons.
| Built-in Validator | Description | Usage |
|---|---|---|
Validators.required | Field must not be empty | required attribute or Validators.required |
Validators.minLength(n) | Minimum character count | minlength="3" or Validators.minLength(3) |
Validators.maxLength(n) | Maximum character count | maxlength="50" or Validators.maxLength(50) |
Validators.email | Valid email format | email attribute or Validators.email |
Validators.pattern(regex) | Must match regex pattern | Validators.pattern(/^[0-9]+$/) |
Validators.min(n) | Minimum numeric value | Validators.min(0) |
Validators.max(n) | Maximum numeric value | Validators.max(100) |
Reactive Form Validation
In reactive forms, validators are added programmatically in the component class. This gives full control over validation logic and makes it easy to test.
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './register.component.html'
})
export class RegisterComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
age: [null, [Validators.required, Validators.min(18), Validators.max(120)]]
});
// Convenience getters for template access
get name() { return this.form.get('name')!; }
get email() { return this.form.get('email')!; }
get password() { return this.form.get('password')!; }
get age() { return this.form.get('age')!; }
onSubmit() {
if (this.form.valid) {
console.log('Form submitted:', this.form.value);
} else {
this.form.markAllAsTouched();
}
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label>Name</label>
<input formControlName="name" />
@if (name.invalid && name.touched) {
@if (name.errors?.['required']) {
<span class="text-danger">Name is required.</span>
}
@if (name.errors?.['minlength']) {
<span class="text-danger">Name must be at least 2 characters.</span>
}
}
</div>
<div>
<label>Email</label>
<input formControlName="email" type="email" />
@if (email.invalid && email.touched) {
@if (email.errors?.['required']) {
<span class="text-danger">Email is required.</span>
}
@if (email.errors?.['email']) {
<span class="text-danger">Enter a valid email address.</span>
}
}
</div>
<button type="submit" [disabled]="form.invalid">Register</button>
</form>
Custom Validators
When built-in validators are not enough, you can create custom validators. A validator is a function that takes an AbstractControl and returns either null (valid) or a ValidationErrors object (invalid).
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// 1. No whitespace validator
export function noWhitespaceValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const hasWhitespace = (control.value || '').includes(' ');
return hasWhitespace ? { whitespace: true } : null;
};
}
// 2. Password strength validator
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value || '';
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
const isStrong = hasUpper && hasLower && hasNumber && hasSpecial;
return isStrong ? null : { weakPassword: { hasUpper, hasLower, hasNumber, hasSpecial } };
};
}
// 3. Cross-field validator — passwords must match
export function passwordMatchValidator(control: AbstractControl): ValidationErrors | null {
const password = control.get('password');
const confirmPassword = control.get('confirmPassword');
if (!password || !confirmPassword) return null;
return password.value === confirmPassword.value ? null : { passwordMismatch: true };
}
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { noWhitespaceValidator, passwordStrengthValidator, passwordMatchValidator } from './custom-validators';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="username" placeholder="Username" />
@if (form.get('username')?.errors?.['whitespace']) {
<span class="text-danger">No spaces allowed.</span>
}
<input formControlName="password" type="password" placeholder="Password" />
@if (form.get('password')?.errors?.['weakPassword']) {
<span class="text-danger">Password needs uppercase, lowercase, number and special char.</span>
}
<input formControlName="confirmPassword" type="password" placeholder="Confirm Password" />
@if (form.errors?.['passwordMismatch']) {
<span class="text-danger">Passwords do not match.</span>
}
<button type="submit" [disabled]="form.invalid">Sign Up</button>
</form>
`
})
export class SignupComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
username: ['', [Validators.required, noWhitespaceValidator()]],
password: ['', [Validators.required, passwordStrengthValidator()]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator });
onSubmit() {
if (this.form.valid) console.log(this.form.value);
}
}
Form Control States
Angular tracks the state of each form control. Understanding these states helps you show the right error messages at the right time.
| State | Property | Meaning |
|---|---|---|
| Pristine | control.pristine | User has not changed the value yet |
| Dirty | control.dirty | User has changed the value |
| Untouched | control.untouched | User has not focused and left the field |
| Touched | control.touched | User has focused and left the field |
| Valid | control.valid | All validators pass |
| Invalid | control.invalid | At least one validator fails |
| Pending | control.pending | Async validator is running |
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.