Angular has made form validation both powerful and easy-to-use through its built-in directives and validators. Whether you prefer the simplicity of template-driven forms or the robustness of reactive forms, Angular has you covered. This guide shows you how to validate forms, create custom validators, implement cross-field validation, and work with async validators in Angular.
If Angular forms are new to you, make sure you check out this introduction first.
Key Takeaways
- Angular supports both template-driven and reactive approaches for form validation.
- Custom validators can be added for specific validation logic.
- Cross-field validation and async validators enhance flexibility in complex forms.
Angular Form Validation Examples:
Template Driven Approach:
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
user = {name:""}
onSubmit() {
console.log("form submitted!");
}
}
app.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<label>Name</label>
<input type="text" #name="ngModel" [(ngModel)]="user.name" name="name" required minlength="2"/>
<button type="submit" [disabled]="!userForm.form.valid">Submit</button>
</form>
<div *ngIf="name.invalid">
<div *ngIf="name.errors.required">Input is required!</div>
<div *ngIf="name.errors.minlength">Input must be at least 2 characters.</div>
</div>
With template-driven forms, Angular simplifies validation using native HTML validation attributes like required and minlength. Angular automatically creates FormControl instances and applies validators, enabling you to conditionally display error messages based on input status.
Reactive Approach
app.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
userForm = new FormGroup({
"name": new FormControl('', [
Validators.required,
Validators.minLength(2)
])
})
onSubmit() {
console.log("form submitted!");
}
}
app.component.html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>Name</label>
<input type="text" formControlName="name"/>
<button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
<div *ngIf="userForm.invalid">
<div *ngIf="userForm.controls.name.errors.required">Input is required!</div>
<div *ngIf="userForm.controls.name.errors.minlength">Input must be at least 2 characters.</div>
</div>
The reactive approach shifts responsibility for validation logic to the component class. Here, validators like Validators.required and Validators.minLength are applied directly to FormControl instances.
Angular Form Validation On Submit
Normally, Angular keeps validation state synchronous with user input, but you might want to validate only after submission. Here's how to do that:
Template Driven Approach:
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
user = {name:""}
onSubmit(form) {
if (form.valid) {
console.log("form is valid");
} else {
console.log(form.controls.name.errors);
}
}
}
app.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<label>Name</label>
<input type="text" #name="ngModel" [(ngModel)]="user.name" name="name" required minlength="2"/>
<button type="submit">Submit</button>
</form>
In the template-driven approach, you pass the form reference into your submit handler to access and log validation errors.
Reactive Approach:
app.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
userForm = new FormGroup({
"name": new FormControl('', [
Validators.required,
Validators.minLength(2)
])
})
onSubmit() {
if (this.userForm.valid) {
console.log("form is valid");
} else {
console.log(this.userForm.controls.name.errors);
}
}
}
app.component.html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>Name</label>
<input type="text" formControlName="name"/>
<button type="submit">Submit</button>
</form>
The reactive form handles validation logic within the component, eliminating the need to reference the form from the template.
Custom Validator Example
Sometimes, standard validation isn't enough. Let's add a custom validator to disallow specific names like 'Bob' or 'Joe'.
Template Driven Approach:
app.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<label>Name</label>
<input type="text" #name="ngModel" appInvalidEntry="Joe" [(ngModel)]="user.name" name="name" required minlength="2"/>
<button type="submit">Submit</button>
</form>
invalid-entry.directive.ts
import { Directive, Input } from '@angular/core';
import { ValidatorFn, AbstractControl, Validator, NG_VALIDATORS } from '@angular/forms';
@Directive({
selector: '[appInvalidEntry]',
providers: [{provide: NG_VALIDATORS, useExisting: InvalidEntryDirective, multi: true}]
})
export class InvalidEntryDirective implements Validator {
@Input('appInvalidEntry') invalidEntry: string;
validate(control: AbstractControl): {[key: string]: any} | null {
return this.invalidEntry ? invalidEntryValidator(new RegExp(this.invalidEntry, 'i'))(control) : null;
}
}
export function invalidEntryValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const invalid = nameRe.test(control.value);
return invalid ? {'invalidEntry': {value: control.value}} : null;
};
}
Create a custom directive for the template-driven approach, implementing the Validator interface. This enables you to define validator logic as a directive attribute.
Reactive Approach:
app.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { invalidEntryValidator } from './invalid-entry.directive';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
userForm = new FormGroup({
"name": new FormControl('', [
Validators.required,
Validators.minLength(2),
invalidEntryValidator(/Joe/i)
])
})
onSubmit() {
if (this.userForm.valid) {
console.log("form is valid");
} else {
console.log(this.userForm.controls.name.errors);
}
}
}
app.component.html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>Name</label>
<input type="text" formControlName="name"/>
<button type="submit">Submit</button>
</form>
In the reactive approach, include the custom validator directly in the list of Validator functions for your FormControl.
Cross Field Validation Example
Ensure a user's first name and last name don't match through cross-field validation.
The Template Driven Approach
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
user = {name:""}
onSubmit(form) {
if (form.valid) {
console.log("form is valid");
} else {
console.log(form.errors);
}
}
}
app.component.html
<form #userForm="ngForm" appInvalidEntry (ngSubmit)="onSubmit(userForm)">
<label>First</label>
<input type="text" #name="ngModel" [(ngModel)]="user.first" name="first" required minlength="2"/>
<label>Last</label>
<input type="text" #age="ngModel" [(ngModel)]="user.last" name="last"/>
<button type="submit">Submit</button>
</form>
invalid-entry.directive.ts
import { Directive, Input } from '@angular/core';
import { ValidatorFn, AbstractControl, Validator, NG_VALIDATORS, FormGroup, ValidationErrors } from '@angular/forms';
@Directive({
selector: '[appInvalidEntry]',
providers: [{provide: NG_VALIDATORS, useExisting: InvalidEntryDirective, multi: true}]
})
export class InvalidEntryDirective implements Validator {
validate(control: AbstractControl): ValidationErrors {
return matchingInputValidator(control)
}
}
export const matchingInputValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
const first = control.get('first');
const last = control.get('last');
return first && last && first.value === last.value ? {'matchingInputs': true} : null;
};
For cross-field validation, modify your directive to include a function that evaluates the entire form group, ensuring context for multiple fields validation.
The Reactive Approach
app.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { matchingInputValidator } from './invalid-entry.directive';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
userForm = new FormGroup({
"first": new FormControl('', [
Validators.required,
Validators.minLength(2)]),
"last": new FormControl('')
}, matchingInputValidator)
onSubmit() {
if (this.userForm.valid) {
console.log("form is valid");
} else {
console.log(this.userForm.errors);
}
}
}
app.component.html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>First</label>
<input type="text" formControlName="first"/>
<label>Last</label>
<input type="text" formControlName="last"/>
<button type="submit">Submit</button>
</form>
Include the cross-field validator as a second argument to the reactive form's FormGroup initializer, ensuring it's applied to the entire group context.
Async Validator Example
Asynchronous validators handle scenarios like server-side validation checks, more important than ever in database-driven validations:
The Template Driven Approach
invalid-entry.directive.ts
import { Directive, Input } from '@angular/core';
import { ValidatorFn, AbstractControl, Validator, NG_VALIDATORS, AsyncValidator } from '@angular/forms';
import { UserService } from './user.service';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Directive({
selector: '[appInvalidEntry]',
providers: [{provide: NG_VALIDATORS, useExisting: InvalidEntryDirective, multi: true}]
})
export class InvalidEntryDirective implements AsyncValidator {
constructor(private userService: UserService) {}
validate(control: AbstractControl): Promise | Observable {
return this.userService.nameExists(control.value).pipe(
map(nameExists => (nameExists ? {nameExists: true} : null)),
catchError(() => null)
);
}
}
Transform validation functions to be asynchronous by utilizing AsyncValidator interface, which can check database constraints asynchronously.
The Reactive Approach
app.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { InvalidEntryAsync } from './invalid-entry.directive';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private invalidEntry: InvalidEntryAsync) {}
userForm = new FormGroup({
"first": new FormControl('', [
Validators.required,
Validators.minLength(2)]),
"last": new FormControl('')
}, {asyncValidators: [this.invalidEntry.validate.bind(this.invalidEntry)]})
onSubmit() {
if (this.userForm.valid) {
console.log("form is valid");
} else {
console.log(this.userForm.errors);
}
}
}
app.component.html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<label>First</label>
<input type="text" formControlName="first"/>
<label>Last</label>
<input type="text" formControlName="last"/>
<button type="submit">Submit</button>
</form>
invalid-entry.directive.ts
import { Injectable, Directive } from '@angular/core';
import { ValidatorFn, AbstractControl, Validator, NG_VALIDATORS, AsyncValidator } from '@angular/forms';
import { UserService } from './user.service';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class InvalidEntryAsync implements AsyncValidator {
constructor(private userService: UserService) {}
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.userService.nameExists(control.value).pipe(
map(nameExists => (nameExists ? {alreadyExists: true} : null)),
catchError(() => null)
);
}
}
@Directive({
selector: '[appInvalidEntry]',
providers: [{provide: NG_VALIDATORS, useExisting: InvalidEntryDirective, multi: true}]
})
export class InvalidEntryDirective implements AsyncValidator {
constructor(private userService: UserService) {}
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.userService.nameExists(control.value).pipe(
map(nameExists => (nameExists ? {alreadyExists: true} : null)),
catchError(() => null)
);
}
}
Use an @Injectable service to handle async logic and integrate it into your form with AsyncValidator.
FAQ
What's the difference between template-driven and reactive forms in Angular?
Template-driven forms rely on directives in your template to wire up validation, while reactive forms let you manage control logic more explicitly in your component classes.
Can I use both reactive and template-driven approaches in the same Angular application?
Yes, you can mix both approaches within the same application, although it's recommended to stay consistent within a single form to maintain code clarity.
How do I create an async validator that checks available usernames from a server?
Use an AsyncValidator implementation that calls a backend service and returns an observable or promise, integrating it within your form controls using Angular's reactive or template-driven paradigms.
Are there performance implications when using async validators?
Async validators can incur performance costs, depending on network latency and server response times. Caching and debouncing strategies can mitigate this.
