On this page
Reactive forms with validation
Reactive forms
Reactive forms define the form structure in TypeScript, giving you full control over validation, state, and transformations.
Setting up Reactive Forms
Import ReactiveFormsModule in the component:
import { ReactiveFormsModule } from '@angular/forms';
@Component({
imports: [ReactiveFormsModule],
// ...
})FormBuilder
FormBuilder is a service that simplifies control creation:
import { inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
const fb = inject(FormBuilder);
const form = fb.nonNullable.group({
name: ['', Validators.required],
age: [0, [Validators.required, Validators.min(18)]],
});Control types
| Type | Usage | Example |
|---|---|---|
FormControl |
A single value | Text input, checkbox |
FormGroup |
Group of controls | Complete form |
FormArray |
Dynamic list | Add/remove fields |
FormArray example
readonly form = this.fb.nonNullable.group({
title: [''],
tags: this.fb.array([
this.fb.control('angular'),
this.fb.control('typescript'),
]),
});
addTag(value: string): void {
const tags = this.form.controls.tags;
tags.push(this.fb.control(value));
}Built-in validators
Angular includes common validators:
import { Validators } from '@angular/forms';
fb.control('', [
Validators.required, // Required field
Validators.minLength(3), // Minimum length
Validators.maxLength(50), // Maximum length
Validators.email, // Email format
Validators.min(0), // Minimum value
Validators.max(100), // Maximum value
Validators.pattern(/^\d+$/), // Regular expression
]);Custom validators
Create functions that receive an AbstractControl and return errors or null:
function corporateEmail(control: AbstractControl): ValidationErrors | null {
const email: string = control.value;
if (!email) return null;
return email.endsWith('@company.com')
? null
: { notCorporateEmail: true };
}Displaying errors in the template
@if (form.controls.email.touched
&& form.controls.email.errors; as errors) {
@if (errors['required']) {
<span class="error">Required field</span>
} @else if (errors['email']) {
<span class="error">Invalid email format</span>
}
}Form state
| Property | Description |
|---|---|
.valid |
All controls are valid |
.invalid |
At least one control is invalid |
.touched |
The user interacted with the control |
.dirty |
The value was modified |
.pristine |
The value has not been modified |
Practice
- Create a contact form: Use
FormBuilderto build a form with name, email, and message fields. AddValidators.requiredandValidators.emailwhere appropriate. - Implement a custom validator: Create a validator function that checks whether a text field contains forbidden words. Attach it to a form control.
- Display errors conditionally: Use
@ifwith.touchedand.errorsto show specific error messages below each field only after the user has interacted with them.
In the next lesson, we will learn how to connect with APIs using HttpClient.
nonNullable
Use fb.nonNullable.group() so that controls don't accept null. This way getRawValue() returns strict types without null, improving TypeScript inference.
Don't mix approaches
Don't combine Template-driven forms (ngModel) with Reactive forms (formGroup) in the same form. Choose one or the other. Angular 21 recommends Reactive forms for complex forms.
import {
Component, inject, signal,
ChangeDetectionStrategy,
} from '@angular/core';
import {
ReactiveFormsModule, FormBuilder,
Validators, AbstractControl, ValidationErrors,
} from '@angular/forms';
// Custom validator
function strongPassword(control: AbstractControl): ValidationErrors | null {
const value: string = control.value;
if (!value) return null;
const hasNumber = /\d/.test(value);
const hasUppercase = /[A-Z]/.test(value);
const hasMinLength = value.length >= 8;
if (hasNumber && hasUppercase && hasMinLength) return null;
return {
weakPassword: {
requiresNumber: !hasNumber,
requiresUppercase: !hasUppercase,
requiresMinLength: !hasMinLength,
},
};
}
@Component({
selector: 'app-register',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule],
templateUrl: './register.html',
})
export class Register {
private readonly fb = inject(FormBuilder);
readonly submitted = signal(false);
readonly form = this.fb.nonNullable.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, strongPassword]],
acceptTerms: [false, Validators.requiredTrue],
});
submit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const data = this.form.getRawValue();
console.log('Registration:', data);
this.submitted.set(true);
}
}
Sign in to track your progress