Overview
Angular provides a robust validation system for both template-driven and reactive forms. Validation ensures user input meets your requirements before processing.
Built-in Validators
For Reactive Forms
typescript
import { Validators } from '@angular/forms';
this.fb.group({
name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.required, Validators.min(18), Validators.max(120)]],
website: ['', Validators.pattern(/^https?:\/\/.+/)],
password: ['', [Validators.required, Validators.minLength(8)]],
});Validator Reference
| Validator | Purpose | Example |
|---|---|---|
Validators.required | Must have a value | Non-empty field |
Validators.requiredTrue | Must be true | Checkbox agreement |
Validators.email | Valid email format | user@domain.com |
Validators.min(n) | Minimum number value | Age ≥ 18 |
Validators.max(n) | Maximum number value | Quantity ≤ 100 |
Validators.minLength(n) | Minimum string length | Name ≥ 2 chars |
Validators.maxLength(n) | Maximum string length | Bio ≤ 500 chars |
Validators.pattern(regex) | matches regex | Phone format |
Displaying Error Messages
Helper Method
typescript
export class MyFormComponent {
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
// Helper to check if a control has a specific error
hasError(controlName: string, errorName: string): boolean {
const control = this.form.get(controlName);
return control ? control.hasError(errorName) && control.touched : false;
}
}html
<div>
<label>Email</label>
<input formControlName="email" type="email">
@if (hasError('email', 'required')) {
<small class="error">Email is required</small>
}
@if (hasError('email', 'email')) {
<small class="error">Please enter a valid email</small>
}
</div>
<div>
<label>Password</label>
<input formControlName="password" type="password">
@if (hasError('password', 'required')) {
<small class="error">Password is required</small>
}
@if (hasError('password', 'minlength')) {
<small class="error">
Password must be at least
{{ form.get('password')?.errors?.['minlength']?.requiredLength }} characters
</small>
}
</div>Custom Validators
Synchronous Validator
typescript
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Validator factory function
export function forbiddenNameValidator(forbidden: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const isForbidden = control.value?.toLowerCase() === forbidden.toLowerCase();
return isForbidden ? { forbiddenName: { value: control.value } } : null;
};
}
// Simple validator function
export function noWhitespaceValidator(control: AbstractControl): ValidationErrors | null {
const isWhitespace = (control.value || '').trim().length === 0;
return isWhitespace ? { whitespace: true } : null;
}
// Strong password validator
export function strongPasswordValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const errors: ValidationErrors = {};
if (!/[A-Z]/.test(value)) errors['missingUppercase'] = true;
if (!/[a-z]/.test(value)) errors['missingLowercase'] = true;
if (!/[0-9]/.test(value)) errors['missingNumber'] = true;
if (!/[!@#$%^&*]/.test(value)) errors['missingSpecial'] = true;
return Object.keys(errors).length ? errors : null;
}Using Custom Validators
typescript
form = this.fb.group({
username: ['', [
Validators.required,
noWhitespaceValidator,
forbiddenNameValidator('admin'),
]],
password: ['', [
Validators.required,
Validators.minLength(8),
strongPasswordValidator,
]],
});html
<div>
<label>Password</label>
<input formControlName="password" type="password">
@if (form.get('password')?.touched && form.get('password')?.invalid) {
<div class="password-requirements">
<p [class.valid]="!form.get('password')?.hasError('missingUppercase')">
✓ Uppercase letter
</p>
<p [class.valid]="!form.get('password')?.hasError('missingLowercase')">
✓ Lowercase letter
</p>
<p [class.valid]="!form.get('password')?.hasError('missingNumber')">
✓ Number
</p>
<p [class.valid]="!form.get('password')?.hasError('missingSpecial')">
✓ Special character (!@#$%^&*)
</p>
</div>
}
</div>Cross-Field Validation
Validate relationships between fields using group-level validators:
Password Match Validator
typescript
export function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
if (password !== confirmPassword) {
return { passwordMismatch: true };
}
return null;
}typescript
form = this.fb.group({
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
}, {
validators: [passwordMatchValidator],
});html
<form [formGroup]="form">
<input formControlName="password" type="password" placeholder="Password">
<input formControlName="confirmPassword" type="password" placeholder="Confirm">
@if (form.hasError('passwordMismatch') && form.get('confirmPassword')?.touched) {
<small class="error">Passwords do not match</small>
}
</form>Date Range Validator
typescript
export function dateRangeValidator(group: AbstractControl): ValidationErrors | null {
const start = group.get('startDate')?.value;
const end = group.get('endDate')?.value;
if (start && end && new Date(start) >= new Date(end)) {
return { invalidDateRange: true };
}
return null;
}Async Validators
Async validators perform validation that requires a backend call (e.g., checking username availability):
typescript
import { AsyncValidatorFn } from '@angular/forms';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs';
export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl) => {
return control.valueChanges.pipe(
debounceTime(400),
switchMap(value => userService.checkEmailExists(value)),
map(exists => exists ? { emailTaken: true } : null),
catchError(() => of(null)),
first(),
);
};
}Simpler Approach
typescript
export function uniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl) => {
return userService.checkUsername(control.value).pipe(
map(isTaken => isTaken ? { usernameTaken: true } : null),
catchError(() => of(null)),
);
};
}Using Async Validators
typescript
export class RegisterComponent {
private userService = inject(UserService);
form = this.fb.group({
username: ['',
[Validators.required, Validators.minLength(3)], // sync validators
[uniqueUsernameValidator(this.userService)], // async validators
],
email: ['', [Validators.required, Validators.email]],
});
}html
<input formControlName="username" placeholder="Username">
@if (form.get('username')?.pending) {
<small>Checking availability...</small>
}
@if (form.get('username')?.hasError('usernameTaken')) {
<small class="error">Username is already taken</small>
}Reusable Error Message Component
typescript
@Component({
selector: 'app-field-error',
standalone: true,
template: `
@if (control?.invalid && control?.touched) {
<div class="field-errors">
@if (control.hasError('required')) {
<small>{{ label }} is required</small>
}
@if (control.hasError('email')) {
<small>Enter a valid email address</small>
}
@if (control.hasError('minlength')) {
<small>Minimum {{ control.errors?.['minlength']?.requiredLength }} characters</small>
}
@if (control.hasError('maxlength')) {
<small>Maximum {{ control.errors?.['maxlength']?.requiredLength }} characters</small>
}
@if (control.hasError('min')) {
<small>Minimum value is {{ control.errors?.['min']?.min }}</small>
}
@if (control.hasError('pattern')) {
<small>Invalid format</small>
}
</div>
}
`,
styles: [`
.field-errors small {
color: #dc3545;
display: block;
font-size: 0.85em;
margin-top: 4px;
}
`],
})
export class FieldErrorComponent {
@Input() control: AbstractControl | null = null;
@Input() label = 'Field';
}html
<!-- Usage -->
<input formControlName="email">
<app-field-error [control]="form.get('email')" label="Email" />Validation Best Practices
- Validate on blur — show errors after the user leaves the field, not while typing
- Show one error at a time — prioritize the most important validation message
- Use async validators sparingly — they trigger API calls on every change
- Debounce async validators — add
debounceTime(300)to avoid excessive API calls - Create reusable validator functions — keep them in a separate file
- Disable submit button when the form is invalid
- Provide clear error messages — tell the user exactly what's wrong and how to fix it
Next Steps
With forms and validation complete, let's explore RxJS — the reactive programming library that powers Angular's data flow, event handling, and asynchronous operations.