Built-in Validators
Most form validation needs — a required field, a minimum length, a numeric range, a well-formed email address — are covered out of the box by Angular’s Validators class. These are pure, synchronous functions you attach to a form control; Angular runs them on every value change and reflects the result in the control’s errors, valid, and invalid state. Knowing the built-in set well means you write less custom code and get consistent, predictable validation across reactive and template-driven forms.
The Validators class
In reactive forms you pass validator functions as the second argument to a FormControl (a single function or an array). Each validator returns either null when the value is valid, or a ValidationErrors object (a key/value map) describing what failed. Angular merges those objects so a control can carry several errors at once.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, 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({
name: ['', [Validators.required, Validators.minLength(2)]],
age: [null, [Validators.required, Validators.min(18), Validators.max(120)]],
email: ['', [Validators.required, Validators.email]],
username: ['', [Validators.required, Validators.pattern(/^[a-z0-9_]+$/)]],
});
}
Built-in validator reference
| Validator | Use it for | Error key | Error payload |
|---|---|---|---|
Validators.required | A value must be present (non-empty, non-null, non-false) | required | true |
Validators.requiredTrue | A checkbox must be ticked (e.g. accept terms) | required | true |
Validators.min(n) | Numeric value must be >= n | min | { min, actual } |
Validators.max(n) | Numeric value must be <= n | max | { max, actual } |
Validators.minLength(n) | String/array length must be >= n | minlength | { requiredLength, actualLength } |
Validators.maxLength(n) | String/array length must be <= n | maxlength | { requiredLength, actualLength } |
Validators.email | Value must look like an email address | email | true |
Validators.pattern(regex) | Value must match a regular expression | pattern | { requiredPattern, actualValue } |
Validators.nullValidator | A no-op (always valid); useful as a placeholder | — | — |
The error payload matters: because
minlengthgives yourequiredLengthandactualLength, you can build messages like “Needs 8 characters, you have 5” without hard-coding numbers in the template.
Reading validation state in the template
With the new control flow, render messages by checking specific error keys. Guard on touched (or dirty) so users don’t see errors before they’ve interacted with a field.
<form [formGroup]="form">
<input formControlName="name" placeholder="Name" />
@if (form.controls.name.touched && form.controls.name.invalid) {
@if (form.controls.name.hasError('required')) {
<small class="error">Name is required.</small>
}
@if (form.controls.name.hasError('minlength')) {
<small class="error">
At least
{{ form.controls.name.getError('minlength').requiredLength }}
characters.
</small>
}
}
<input formControlName="age" type="number" placeholder="Age" />
@if (form.controls.age.hasError('min')) {
<small class="error">You must be 18 or older.</small>
}
</form>
Composing validators
Validators.compose([...]) merges several validators into one function, which is handy when you store reusable validator bundles. Passing an array directly to a control does the same thing, so reach for compose only when you need a single callable.
import { Validators, ValidatorFn } from '@angular/forms';
export const strongName: ValidatorFn = Validators.compose([
Validators.required,
Validators.minLength(2),
Validators.maxLength(50),
])!;
Template-driven forms
In template-driven forms you don’t call Validators directly — you use HTML5-style attribute directives, and Angular wires them to the same underlying validator functions. The error keys produced are identical, so your message logic carries over.
<form #f="ngForm">
<input
name="email"
ngModel
required
email
#email="ngModel"
/>
@if (email.touched && email.invalid) {
@if (email.hasError('required')) {
<small class="error">Email is required.</small>
}
@if (email.hasError('email')) {
<small class="error">Enter a valid email address.</small>
}
}
</form>
Available attribute directives mirror the class: required, minlength, maxlength, min, max, email, and pattern. Bind dynamic limits with property syntax, e.g. [minlength]="minChars".
Inspecting errors at runtime
Logging a control’s errors object is the fastest way to see exactly what a validator reports.
const ctrl = this.form.controls.age;
ctrl.setValue(12);
console.log(ctrl.errors);
Output:
{ min: { min: 18, actual: 12 } }
Best practices
- Order validators from cheapest to most specific (
requiredfirst) — Angular runs all of them, but reading them in that order keeps intent clear. - Prefer the error payload (
requiredLength,min,max) over magic numbers so messages stay in sync with the rules. - Gate error display on
touchedordirtyto avoid flagging untouched fields on first render. - Use
Validators.requiredTruefor “accept terms” checkboxes — plainrequiredpasses when the box is unchecked-but-present in some setups. - Treat
Validators.emailandpatternas a first line of defence only; always re-validate on the server. - Keep template-driven and reactive validation consistent by relying on the same error keys for shared message components.