AngularJSForm Validation

Angular Form Validation

Form validation ensures users provide correct, complete data before it is submitted. Angular's validation pipeline runs synchronously or asynchronously, integrates with both template-driven and reactive forms, and automatically tracks per-control error state.

This page covers built-in validators, error display patterns, cross-field validation, and async validation.

Built-in Validators

Validator

Template attribute

Reactive import

What it checks

required

required

Validators.required

Non-empty value

email

email

Validators.email

Valid email format

minlength

minlength="N"

Validators.minLength(N)

String length >= N

maxlength

maxlength="N"

Validators.maxLength(N)

String length <= N

min

min="N"

Validators.min(N)

Number >= N

max

max="N"

Validators.max(N)

Number <= N

pattern

pattern="regex"

Validators.pattern(regex)

Matches regular expression

nullValidator

Validators.nullValidator

Always valid (no-op)

Applying Validators in Reactive Forms

TS
import { FormGroup, FormControl, Validators } from '@angular/forms';

const form = new FormGroup({
  username: new FormControl('', [
    Validators.required,
    Validators.minLength(3),
    Validators.maxLength(20),
    Validators.pattern(/^[a-zA-Z0-9_]+$/),
  ]),
  age: new FormControl<number | null>(null, [
    Validators.required,
    Validators.min(18),
    Validators.max(120),
  ]),
  website: new FormControl('', [
    Validators.pattern(/^https?:\/\/.+/),
  ]),
});
Displaying Validation Errors

Show errors only after the user has interacted with the field (touched) or tried to submit the form — not while they are still typing for the first time.

HTML
<form [formGroup]="form" (ngSubmit)="onSubmit()">

  <div>
    <label for="username">Username</label>
    <input id="username" formControlName="username" />

    <!-- Show individual error messages -->
    <div *ngIf="form.get('username') as ctrl">
      <ng-container *ngIf="ctrl.invalid && (ctrl.dirty || ctrl.touched)">
        <p *ngIf="ctrl.hasError('required')">Username is required.</p>
        <p *ngIf="ctrl.hasError('minlength')">
          Minimum {{ ctrl.getError('minlength').requiredLength }} characters.
        </p>
        <p *ngIf="ctrl.hasError('maxlength')">
          Maximum {{ ctrl.getError('maxlength').requiredLength }} characters.
        </p>
        <p *ngIf="ctrl.hasError('pattern')">
          Only letters, numbers, and underscores allowed.
        </p>
      </ng-container>
    </div>
  </div>

  <button type="submit" [disabled]="form.invalid">Register</button>
</form>
Note
ctrl.getError('minlength') returns the error details object: { requiredLength: 3, actualLength: 1 }. Use it to show helpful, specific messages rather than generic ones.
Marking All Controls Touched on Submit

Users sometimes skip fields and click Submit. Call markAllAsTouched() on submit to trigger error display for every unfilled field.

TS
onSubmit() {
  if (this.form.invalid) {
    this.form.markAllAsTouched();  // reveal errors on all controls
    return;
  }
  // process form.value
}
Cross-Field (Group-Level) Validation

Some validations span multiple fields — confirming a password, ensuring start date is before end date. Apply these validators to the FormGroup rather than an individual control.

TS
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Cross-field validator function
export const passwordMatchValidator: ValidatorFn = (
  group: AbstractControl
): ValidationErrors | null => {
  const password = group.get('password')?.value;
  const confirm  = group.get('confirmPassword')?.value;

  if (password && confirm && password !== confirm) {
    return { passwordMismatch: true };
  }
  return null;
};

// Apply to FormGroup
const form = new FormGroup(
  {
    password:        new FormControl('', [Validators.required, Validators.minLength(8)]),
    confirmPassword: new FormControl('', Validators.required),
  },
  { validators: passwordMatchValidator }  // group-level validator
);

HTML
<!-- Display group-level error -->
<p *ngIf="form.hasError('passwordMismatch') && form.get('confirmPassword')?.touched">
  Passwords do not match.
</p>
Async Validation

Async validators check the server — for example, whether a username is already taken. They return an Observable or Promise.

TS
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class UniqueEmailValidator {
  constructor(private http: HttpClient) {}

  validate(): AsyncValidatorFn {
    return (ctrl: AbstractControl): Observable<ValidationErrors | null> => {
      if (!ctrl.value) return of(null);

      return of(ctrl.value).pipe(
        debounceTime(400),           // wait for user to stop typing
        switchMap(email =>
          this.http.get<{ exists: boolean }>(`/api/email-check?q=${email}`)
        ),
        map(res => (res.exists ? { emailTaken: true } : null)),
        catchError(() => of(null)),  // don't block submission on network error
        first(),                     // complete the observable
      );
    };
  }
}

TS
// Attach async validator to a FormControl
import { inject } from '@angular/core';

export class SignupComponent {
  private emailValidator = inject(UniqueEmailValidator);

  form = new FormGroup({
    email: new FormControl('', {
      validators: [Validators.required, Validators.email],
      asyncValidators: [this.emailValidator.validate()],
      updateOn: 'blur',  // only validate when focus leaves — reduces requests
    }),
  });
}

HTML
<input formControlName="email" />

<!-- Pending state while async validator runs -->
<span *ngIf="form.get('email')?.pending">Checking availability...</span>

<!-- Async error -->
<span *ngIf="form.get('email')?.hasError('emailTaken')">
  This email is already registered.
</span>
updateOn Strategy

By default, Angular validates on every keystroke. You can change this per-control or per-form.

updateOn

When validation runs

change (default)

On every input event (every keystroke)

blur

When the field loses focus

submit

Only when the form is submitted

TS
// Per-control
new FormControl('', {
  validators: Validators.required,
  updateOn: 'blur',
})

// Per-form — all controls inherit this default
new FormGroup({ ... }, { updateOn: 'submit' })
Template-Driven Validation

Template-driven forms use HTML attributes for validation. Use a template reference to access the control's state.

HTML
<form #f="ngForm" (ngSubmit)="onSubmit(f)">

  <input
    name="email"
    type="email"
    ngModel
    #emailField="ngModel"
    required
    email
  />

  <div *ngIf="emailField.invalid && emailField.touched">
    <p *ngIf="emailField.errors?.['required']">Email is required.</p>
    <p *ngIf="emailField.errors?.['email']">Enter a valid email address.</p>
  </div>

  <button [disabled]="f.invalid">Submit</button>
</form>
Reactive Error Helper Pattern

A common pattern is to create a getter for each field to reduce verbose template expressions.

TS
export class SignupComponent {
  form = this.fb.group({
    email:    ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
  });

  // Concise getters used in template
  get email()    { return this.form.get('email')!; }
  get password() { return this.form.get('password')!; }
}

HTML
<!-- Clean template using getters -->
<input formControlName="email" [class.error]="email.invalid && email.touched" />
<p *ngIf="email.hasError('required') && email.touched">Email required.</p>
<p *ngIf="email.hasError('email') && email.touched">Invalid email format.</p>

<input type="password" formControlName="password" />
<p *ngIf="password.hasError('minlength') && password.touched">
  At least {{ password.getError('minlength').requiredLength }} characters.
</p>
Tip
Create a reusable FieldErrorComponent that accepts a FormControl input and renders the right error message automatically — this eliminates duplicated error markup across your forms.
Combining Multiple Validators

TS
import { Validators } from '@angular/forms';

// Compose validators with an array
const emailControl = new FormControl('', [
  Validators.required,
  Validators.email,
  Validators.maxLength(254),
]);

// Or use Validators.compose() (same result, explicit)
const emailControl2 = new FormControl(
  '',
  Validators.compose([
    Validators.required,
    Validators.email,
    Validators.maxLength(254),
  ])
);
Warning
Angular evaluates sync validators in the array order and stops at the first failing validator for each validation run — it does NOT stop accumulating errors. All validators run, and all errors are added to the errors object simultaneously. Use this to show multiple error messages at once.

Summary: Angular's validation pipeline supports synchronous validators on individual controls, asynchronous validators for server-side checks, and group-level validators for cross-field rules. Always show errors only after the user has touched a field or tried to submit, and call markAllAsTouched() on a failed submit to reveal all unfilled fields at once.