Angular Forms Overview
Forms are central to almost every web application — login screens, search boxes, settings panels, checkout flows. Angular provides two powerful, built-in approaches for building forms:
- Template-Driven Forms — declare form logic in HTML with directives; minimal TypeScript
- Reactive Forms — declare form logic entirely in TypeScript; HTML just connects to the model
Both approaches integrate with Angular's change detection, validation pipeline, and accessibility support. Choosing the right one depends on your form's complexity and your team's preferences.
Template-Driven Forms at a Glance
Template-driven forms use Angular directives (ngModel, ngForm, required, minlength, etc.) directly in the template. Angular creates the underlying form model automatically.
// Template-driven — minimal component code
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [FormsModule],
template: `
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
<input name="email" ngModel required email />
<button type="submit" [disabled]="f.invalid">Submit</button>
</form>
`,
})
export class LoginComponent {
onSubmit(value: unknown) {
console.log(value);
}
}Reactive Forms at a Glance
Reactive forms build the form model in the component class using FormGroup, FormControl, and FormArray. The template simply binds to these objects.
// Reactive — form defined in TypeScript
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" />
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`,
})
export class LoginComponent {
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
});
onSubmit() {
console.log(this.form.value);
}
}Comparison: Template-Driven vs Reactive
Aspect | Template-Driven | Reactive |
|---|---|---|
Form model location | Created by Angular from the template | Explicitly built in the component class |
Data flow | Two-way binding with ngModel | Immutable observables via valueChanges |
Validation | HTML attributes + directives | Validator functions in TypeScript |
Dynamic fields | Difficult | Easy with FormArray |
Unit testing | Requires DOM / Angular TestBed | Pure TypeScript, no DOM needed |
Async validation | Supported but awkward | First-class support |
Best for | Simple forms, prototypes | Complex, dynamic, or data-heavy forms |
Import needed | FormsModule | ReactiveFormsModule |
Core Building Blocks
Both form types share these foundational classes from @angular/forms:
FormControl — tracks the value and validation state of a single input element.
FormGroup — a collection of FormControl instances that tracks value and validity as a unit.
FormArray — an ordered list of controls whose length can change dynamically.
AbstractControl — the base class for all three; provides shared properties like value, valid, invalid, pristine, dirty, touched, and errors.
import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms';
// FormControl — single field
const nameControl = new FormControl('', Validators.required);
console.log(nameControl.value); // ''
console.log(nameControl.valid); // false (required + empty)
// FormGroup — object of controls
const addressGroup = new FormGroup({
street: new FormControl(''),
city: new FormControl('', Validators.required),
zip: new FormControl('', Validators.pattern(/^\d{5}$/)),
});
// FormArray — dynamic list of controls
const tagsArray = new FormArray([
new FormControl('angular'),
new FormControl('typescript'),
]);
tagsArray.push(new FormControl('rxjs')); // add controls at runtimeControl State Properties
Angular tracks interaction state on every control. Use these to show errors only after the user has interacted with a field.
Property | Opposite | Meaning |
|---|---|---|
valid | invalid | All validators pass / at least one fails |
pristine | dirty | Value unchanged since init / value has been changed |
untouched | touched | Field never blurred / field has been blurred |
pending | — | Async validator is running |
disabled | enabled | Control is disabled |
<!-- Show error only after user interacts with the field -->
<input formControlName="email" />
<div *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
Please enter a valid email address.
</div>The Validators Class
Angular's built-in Validators covers the most common validation rules. You can compose multiple validators using an array.
import { Validators } from '@angular/forms';
// Single validator
new FormControl('', Validators.required)
// Multiple validators (array)
new FormControl('', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
])
// Built-in validators
Validators.required // value must be non-empty
Validators.email // must match email format
Validators.min(0) // numeric min
Validators.max(100) // numeric max
Validators.minLength(8) // string min length
Validators.maxLength(128) // string max length
Validators.pattern(/regex/) // must match regexAsync Validators
For validations that require a network call — such as checking if a username is already taken — Angular supports async validators. They return a Promise or Observable that resolves to validation errors (or null for valid).
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class UsernameValidator {
constructor(private http: HttpClient) {}
checkAvailability(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) return of(null);
return of(control.value).pipe(
debounceTime(400),
switchMap(username =>
this.http.get<{ available: boolean }>(`/api/users/check/${username}`)
),
map(res => res.available ? null : { usernameTaken: true }),
catchError(() => of(null)),
first(),
);
};
}
}
// Usage in component
username = new FormControl('', {
validators: [Validators.required, Validators.minLength(3)],
asyncValidators: [this.usernameValidator.checkAvailability()],
updateOn: 'blur', // only validate when focus leaves the field
});new FormControl() is always the async validator (or an array of them). Async validators only run after all synchronous validators pass, saving unnecessary network requests.Choosing Between the Two Approaches
Use template-driven forms for simple contact forms, login screens, or quick prototypes
Use reactive forms when you need dynamic field lists (FormArray), complex cross-field validation, or programmatic control
Reactive forms are easier to unit-test because the model is plain TypeScript objects
Both approaches can be used in the same application — pick per form, not per project
The Angular team considers reactive forms to be the "scalable" approach for enterprise apps
FormControl<string>, FormGroup<...>, and FormArray<...> carry full TypeScript generics. This eliminates a large class of runtime errors — always enable strict typing in new projects.Module Setup
// For template-driven forms
import { FormsModule } from '@angular/forms';
// For reactive forms
import { ReactiveFormsModule } from '@angular/forms';
// In a standalone component — import directly
@Component({
standalone: true,
imports: [FormsModule], // or ReactiveFormsModule
template: `...`,
})
export class MyFormComponent {}
// In an NgModule — import once in the module
@NgModule({
imports: [FormsModule, ReactiveFormsModule],
})
export class AppModule {}Summary: Angular offers two complementary form systems. Template-driven forms are quick and declarative; reactive forms are explicit and scalable. Both share the same FormControl/FormGroup/FormArray model and validation pipeline. The following pages dive deep into each approach, validation, custom validators, and more.