Built-in Validators
Angular includes several built-in validators in theValidators class:
required
Requires a non-empty value
Validates email format
min
Minimum numeric value
max
Maximum numeric value
minLength
Minimum string/array length
maxLength
Maximum string/array length
pattern
Validates against regex
requiredTrue
Requires true value (checkboxes)
nullValidator
No-op validator
Using Validators in Reactive Forms
Single Validator
packages/forms/src/validators.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
template: `...`
})
export class SignupComponent {
// Single validator
email = new FormControl('', Validators.required);
// Check validation state
ngOnInit() {
console.log(this.email.errors); // { required: true }
console.log(this.email.valid); // false
}
}
Multiple Validators
packages/forms/src/validators.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-profile',
template: `...`
})
export class ProfileComponent {
constructor(private fb: FormBuilder) {}
profileForm = this.fb.group({
// Multiple validators as array
email: ['', [Validators.required, Validators.email]],
// Numeric validators
age: [null, [Validators.required, Validators.min(18), Validators.max(100)]],
// String length validators
username: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(20)]],
// Pattern validator
phone: ['', [Validators.required, Validators.pattern(/^\d{3}-\d{3}-\d{4}$/)]],
// Required true (for checkboxes)
terms: [false, Validators.requiredTrue]
});
}
Displaying Validation Errors
import { Component } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-registration',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="registrationForm">
<div>
<label for="email">Email:</label>
<input id="email" type="email" formControlName="email">
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<p *ngIf="email.errors?.['required']" style="color: red;">
Email is required
</p>
<p *ngIf="email.errors?.['email']" style="color: red;">
Invalid email format
</p>
</div>
</div>
<div>
<label for="password">Password:</label>
<input id="password" type="password" formControlName="password">
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<p *ngIf="password.errors?.['required']" style="color: red;">
Password is required
</p>
<p *ngIf="password.errors?.['minlength']" style="color: red;">
Password must be at least
{{ password.errors?.['minlength'].requiredLength }} characters
(current: {{ password.errors?.['minlength'].actualLength }})
</p>
</div>
</div>
<button type="submit" [disabled]="registrationForm.invalid">Register</button>
</form>
`
})
export class RegistrationComponent {
constructor(private fb: FormBuilder) {}
registrationForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
get email() {
return this.registrationForm.get('email')!;
}
get password() {
return this.registrationForm.get('password')!;
}
}
Using Validators in Template-Driven Forms
Built-in Validation Directives
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-contact',
standalone: true,
imports: [FormsModule, CommonModule],
template: `
<form #contactForm="ngForm">
<div>
<label for="name">Name:</label>
<input
id="name"
name="name"
type="text"
[(ngModel)]="contact.name"
#nameField="ngModel"
required
minlength="2">
<div *ngIf="nameField.invalid && nameField.touched">
<p *ngIf="nameField.errors?.['required']" style="color: red;">
Name is required
</p>
<p *ngIf="nameField.errors?.['minlength']" style="color: red;">
Name must be at least 2 characters
</p>
</div>
</div>
<div>
<label for="email">Email:</label>
<input
id="email"
name="email"
type="email"
[(ngModel)]="contact.email"
#emailField="ngModel"
required
email>
<div *ngIf="emailField.invalid && emailField.touched">
<p *ngIf="emailField.errors?.['required']" style="color: red;">
Email is required
</p>
<p *ngIf="emailField.errors?.['email']" style="color: red;">
Invalid email format
</p>
</div>
</div>
<div>
<label for="age">Age:</label>
<input
id="age"
name="age"
type="number"
[(ngModel)]="contact.age"
#ageField="ngModel"
min="18"
max="100">
<div *ngIf="ageField.invalid && ageField.touched">
<p *ngIf="ageField.errors?.['min']" style="color: red;">
Must be at least 18
</p>
<p *ngIf="ageField.errors?.['max']" style="color: red;">
Must be 100 or less
</p>
</div>
</div>
<div>
<label for="phone">Phone:</label>
<input
id="phone"
name="phone"
type="tel"
[(ngModel)]="contact.phone"
#phoneField="ngModel"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
<div *ngIf="phoneField.invalid && phoneField.touched">
<p *ngIf="phoneField.errors?.['pattern']" style="color: red;">
Format: 123-456-7890
</p>
</div>
</div>
</form>
`
})
export class ContactComponent {
contact = {
name: '',
email: '',
age: null,
phone: ''
};
}
Custom Validators
Creating a Custom Validator Function
packages/forms/src/validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Validator that checks if password contains a number
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value) {
return null;
}
const hasNumber = /[0-9]/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const passwordValid = hasNumber && hasUpper && hasLower && hasSpecial;
return !passwordValid ? { passwordStrength: {
hasNumber,
hasUpper,
hasLower,
hasSpecial
}} : null;
};
}
// Usage in reactive forms
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-password-form',
template: `
<form [formGroup]="form">
<input type="password" formControlName="password">
<div *ngIf="password.invalid && password.touched">
<p *ngIf="password.errors?.['passwordStrength']">
Password must contain:
<span *ngIf="!password.errors?.['passwordStrength'].hasNumber">number, </span>
<span *ngIf="!password.errors?.['passwordStrength'].hasUpper">uppercase, </span>
<span *ngIf="!password.errors?.['passwordStrength'].hasLower">lowercase, </span>
<span *ngIf="!password.errors?.['passwordStrength'].hasSpecial">special char</span>
</p>
</div>
</form>
`
})
export class PasswordFormComponent {
constructor(private fb: FormBuilder) {}
form = this.fb.group({
password: ['', [Validators.required, Validators.minLength(8), passwordStrengthValidator()]]
});
get password() {
return this.form.get('password')!;
}
}
Cross-Field Validation
Validate multiple fields together:packages/forms/src/validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// Validator that checks if password and confirm password match
export function passwordMatchValidator(): ValidatorFn {
return (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 };
};
}
// Usage
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-signup-form',
template: `
<form [formGroup]="signupForm">
<div>
<label>Password:</label>
<input type="password" formControlName="password">
</div>
<div>
<label>Confirm Password:</label>
<input type="password" formControlName="confirmPassword">
<div *ngIf="signupForm.errors?.['passwordMismatch'] &&
signupForm.get('confirmPassword')?.touched">
<p style="color: red;">Passwords do not match</p>
</div>
</div>
<button type="submit" [disabled]="signupForm.invalid">Sign Up</button>
</form>
`
})
export class SignupFormComponent {
constructor(private fb: FormBuilder) {}
signupForm = this.fb.group({
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator() });
}
Custom Validator Directive
For template-driven forms:packages/forms/src/directives/validators.ts
import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
@Directive({
selector: '[appForbiddenName]',
standalone: true,
providers: [
{ provide: NG_VALIDATORS, useExisting: ForbiddenNameDirective, multi: true }
]
})
export class ForbiddenNameDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
const forbidden = /admin|root|superuser/i.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
}
}
// Usage in template
/*
<input
name="username"
[(ngModel)]="username"
#usernameField="ngModel"
appForbiddenName
required>
<div *ngIf="usernameField.errors?.['forbiddenName']">
<p>This username is not allowed</p>
</div>
*/
Async Validators
Async validators return a Promise or Observable for server-side validation:packages/forms/src/validators.ts
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';
export function uniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return control.valueChanges.pipe(
debounceTime(300),
switchMap(value => userService.checkUsername(value)),
map(isTaken => (isTaken ? { usernameTaken: true } : null)),
catchError(() => of(null))
);
};
}
// Usage
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-username-check',
template: `
<form [formGroup]="form">
<input type="text" formControlName="username">
<div *ngIf="username.pending">
<p>Checking username availability...</p>
</div>
<div *ngIf="username.errors?.['usernameTaken'] && username.touched">
<p style="color: red;">Username is already taken</p>
</div>
<div *ngIf="username.valid">
<p style="color: green;">Username is available!</p>
</div>
</form>
`
})
export class UsernameCheckComponent {
constructor(
private fb: FormBuilder,
private userService: UserService
) {}
form = this.fb.group({
username: [
'',
[Validators.required, Validators.minLength(3)],
[uniqueUsernameValidator(this.userService)]
]
});
get username() {
return this.form.get('username')!;
}
}
Validation Status
Form controls have several status properties:const control = new FormControl('', Validators.required);
// Validation status
console.log(control.valid); // false
console.log(control.invalid); // true
console.log(control.pending); // false (true during async validation)
console.log(control.errors); // { required: true }
// User interaction status
console.log(control.pristine); // true (not changed)
console.log(control.dirty); // false (changed)
console.log(control.untouched); // true (not visited)
console.log(control.touched); // false (visited)
// Overall status string
console.log(control.status); // 'INVALID' | 'VALID' | 'PENDING' | 'DISABLED'
Dynamic Validation
Add or remove validators dynamically:import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-dynamic-validation',
template: `
<form [formGroup]="form">
<label>
<input type="checkbox" (change)="toggleRequired($event)">
Make email required
</label>
<input type="email" formControlName="email">
<p *ngIf="email.invalid && email.touched" style="color: red;">
Email is required
</p>
</form>
`
})
export class DynamicValidationComponent {
constructor(private fb: FormBuilder) {}
form = this.fb.group({
email: ['']
});
get email() {
return this.form.get('email')!;
}
toggleRequired(event: any) {
const emailControl = this.email;
if (event.target.checked) {
// Add validators
emailControl.setValidators([Validators.required, Validators.email]);
} else {
// Remove validators
emailControl.clearValidators();
}
// Must call updateValueAndValidity after changing validators
emailControl.updateValueAndValidity();
}
}
Validation Error Messages
Create reusable error message components:import { Component, Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-validation-errors',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="control.invalid && (control.dirty || control.touched)"
style="color: red; margin-top: 5px;">
<p *ngIf="control.errors?.['required']">This field is required</p>
<p *ngIf="control.errors?.['email']">Invalid email format</p>
<p *ngIf="control.errors?.['minlength']">
Minimum length is {{ control.errors?.['minlength'].requiredLength }}
</p>
<p *ngIf="control.errors?.['maxlength']">
Maximum length is {{ control.errors?.['maxlength'].requiredLength }}
</p>
<p *ngIf="control.errors?.['min']">
Minimum value is {{ control.errors?.['min'].min }}
</p>
<p *ngIf="control.errors?.['max']">
Maximum value is {{ control.errors?.['max'].max }}
</p>
<p *ngIf="control.errors?.['pattern']">
Invalid format
</p>
</div>
`
})
export class ValidationErrorsComponent {
@Input() control!: AbstractControl;
}
// Usage
/*
<input formControlName="email">
<app-validation-errors [control]="form.get('email')!"></app-validation-errors>
*/
Best Practices
Show errors only after user interaction
Show errors only after user interaction
Check
touched or dirty before showing errors to avoid overwhelming users.control.invalid && (control.dirty || control.touched)
Provide clear error messages
Provide clear error messages
Tell users exactly what they need to fix, including requirements.
Use debounce for expensive validators
Use debounce for expensive validators
Debounce async validators to reduce server calls.
control.valueChanges.pipe(debounceTime(300))
Compose validators
Compose validators
Use
Validators.compose() to combine multiple validators.Validators.compose([Validators.required, Validators.email])
Update validators dynamically
Update validators dynamically
Remember to call
updateValueAndValidity() after changing validators.Testing Form Validation
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, LoginComponent]
});
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
});
it('should create form with required validators', () => {
expect(component.loginForm.get('email')?.hasError('required')).toBe(true);
expect(component.loginForm.valid).toBe(false);
});
it('should validate email format', () => {
const emailControl = component.loginForm.get('email');
emailControl?.setValue('invalid-email');
expect(emailControl?.hasError('email')).toBe(true);
emailControl?.setValue('[email protected]');
expect(emailControl?.hasError('email')).toBe(false);
});
it('should mark form as valid when all fields are valid', () => {
component.loginForm.patchValue({
email: '[email protected]',
password: 'password123'
});
expect(component.loginForm.valid).toBe(true);
});
});
Next Steps
Reactive Forms
Learn more about reactive forms
Template-Driven Forms
Learn more about template-driven forms
