Skip to content

Validators and Error Display

Reactive forms become powerful when combined with validation. Angular ships with built-in validators for the most common rules, and you can write custom validators for anything else.

Pass an array of validators as the second element in the control tuple:

import { FormBuilder, Validators } from '@angular/forms';
filterForm = this.fb.group({
query: ['', [Validators.required, Validators.minLength(2)]],
year: [''],
genre: ['']
});

Built-in validators:

ValidatorWhat it checks
Validators.requiredValue is not empty
Validators.minLength(n)Value is at least n characters
Validators.maxLength(n)Value is at most n characters
Validators.min(n)Numeric value is at least n
Validators.max(n)Numeric value is at most n
Validators.emailValue is a valid email format
Validators.pattern(regex)Value matches the regex

Every FormControl tracks its validation state. You can read this state in the template:

// Access a control
get queryControl() {
return this.filterForm.get('query');
}

Control state properties:

PropertyWhat it means
control.validAll validators pass
control.invalidAt least one validator fails
control.dirtyThe user has changed the value
control.touchedThe user has focused and then blurred the field
control.hasError('errorName')A specific error is present

Show errors only after the user has interacted with the field (use touched or dirty to avoid showing errors before they type):

<!-- search form template -->
<form [formGroup]="filterForm">
<input formControlName="query" placeholder="Search..." />
@if (filterForm.get('query')?.invalid && filterForm.get('query')?.touched) {
<div class="errors">
@if (filterForm.get('query')?.hasError('required')) {
<p class="error">Search query is required.</p>
}
@if (filterForm.get('query')?.hasError('minlength')) {
<p class="error">Enter at least 2 characters.</p>
}
</div>
}
<button type="submit" [disabled]="filterForm.invalid">Search</button>
</form>

Reading filterForm.get('query') repeatedly is verbose. Use a getter in the class:

get queryControl() {
return this.filterForm.controls.query;
}

Then in the template:

@if (queryControl.invalid && queryControl.touched) {
@if (queryControl.hasError('required')) {
<p class="error">Required.</p>
}
@if (queryControl.hasError('minlength')) {
<p class="error">Minimum 2 characters.</p>
}
}

Use [disabled]="filterForm.invalid" to prevent form submission until all controls are valid:

<button type="submit" [disabled]="filterForm.invalid">Search</button>

In CinemaVault, the Browse filter form does not use a submit button — changes are applied immediately via valueChanges. But the search form in the NavBar validates that the query is not blank before navigating.

  1. Add Validators.required and Validators.minLength(2) to a query control.
  2. Add a getter get query() { return this.myForm.controls.query; }.
  3. In the template, show an error paragraph when the control is invalid && touched.
  4. Show different messages for required and minlength errors.
  5. Add [disabled]="myForm.invalid" to the submit button.
  6. Tab through the input without typing and confirm the error appears on blur.
  • Add validators as the second element in the control tuple: ['', [Validators.required, Validators.minLength(2)]].
  • Built-in validators include required, minLength, maxLength, email, and pattern.
  • Check validation state with control.invalid, control.touched, and control.hasError('errorName').
  • Show errors only after touched or dirty to avoid alarming users before they interact.
  • Use getters in the class to avoid repeating filterForm.get('fieldName') in the template.