GitHub · LinkedIn · About · YouTube
Last updated by Kindson Munonye — June 29, 2026
Last updated: June 29, 2026 · Estimated time: ~50 minutes · Try the StackBlitz demo →
This is the definitive guide to validation in Angular reactive forms. It combines and extends our Reactive Forms tutorial and Form Validation guide with Angular 19 patterns, custom validators, async checks, and production-ready error handling.
Prerequisites
- Part 1: Template-driven forms (optional background)
- Part 2: Reactive forms — FormBuilder, FormGroup basics
- Node.js 18+, Angular CLI 19
Step 1 — Project setup (standalone component)
ng new validation-demo --routing --style=scss --ssr=false
cd validation-demo
# Add ReactiveFormsModule imports in standalone components// signup.component.ts
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './signup.component.html'
})
export class SignupComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
});
}Step 2 — Built-in validators
| Validator | Use case |
|---|---|
Validators.required | Non-empty value |
Validators.email | Email format |
Validators.minLength(n) | Minimum characters |
Validators.pattern(regex) | Phone, username format |
Validators.min/max | Numeric ranges |
Step 3 — Display validation errors in the template
<input formControlName="email" class="form-control">
<span class="error" *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
<ng-container *ngIf="form.get('email')?.errors?.['required']">Email is required</ng-container>
<ng-container *ngIf="form.get('email')?.errors?.['email']">Enter a valid email</ng-container>
</span>Tip: Use touched or dirty to avoid showing errors on page load. On submit, call this.form.markAllAsTouched().
Step 4 — Custom validators
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function noWhitespace(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const v = (control.value || '').trim();
return v.length ? null : { whitespace: true };
};
}
export function forbiddenValue(forbidden: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null =>
control.value === forbidden ? { forbiddenValue: true } : null;
}username: ['', [Validators.required, noWhitespace(), forbiddenValue('admin')]]Step 5 — Cross-field validation (password match)
function passwordMatch(group: AbstractControl): ValidationErrors | null {
const pass = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return pass === confirm ? null : { passwordMismatch: true };
}
form = this.fb.group({
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatch });<span *ngIf="form.errors?.['passwordMismatch'] && form.get('confirmPassword')?.touched">
Passwords do not match
</span>Step 6 — Async validators (username availability)
import { AsyncValidatorFn } from '@angular/forms';
import { map, delay } from 'rxjs/operators';
import { of } from 'rxjs';
export function usernameTaken(): AsyncValidatorFn {
return (control) =>
of(control.value === 'taken').pipe(
delay(400),
map(isTaken => (isTaken ? { usernameTaken: true } : null))
);
}
username: ['', [Validators.required], [usernameTaken()]]<span *ngIf="form.get('username')?.pending">Checking availability…</span>Step 7 — Validate FormArray (dynamic fields)
skills = this.fb.array([
this.fb.control('', Validators.required)
]);
addSkill() {
this.skills.push(this.fb.control('', Validators.required));
}
get skillsValid() {
return this.skills.controls.every(c => c.valid);
}Step 8 — Dynamic validators with setValidators
onAccountTypeChange(type: 'personal' | 'business') {
const company = this.form.get('companyName');
if (type === 'business') {
company?.setValidators([Validators.required]);
} else {
company?.clearValidators();
}
company?.updateValueAndValidity();
}Step 9 — Submit handling best practices
- Disable submit when
form.invalid || form.pending - Call
markAllAsTouched()on invalid submit - Log
form.errorsand per-control.errorsfor debugging - Use
getRawValue()when disabled controls should be included
onSubmit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log(this.form.getRawValue());
}FAQ
Reactive vs template-driven validation — which should I use?
Use reactive forms when validation logic is complex, testable, or dynamic. Template-driven forms suit simple cases with HTML5 attributes.
Why aren’t my validation errors showing?
Check that the control is touched or call markAllAsTouched(). Ensure formControlName matches the control key exactly.
How do I validate on blur instead of every keystroke?
Use updateOn: 'blur' in the FormControl config: ['', { validators: Validators.required, updateOn: 'blur' }] (Angular 14+).
Practice & related tutorials
- StackBlitz — editable validation demo
- Reactive Forms tutorial (Part 2)
- Form Validation step-by-step
- Angular tutorials hub