Angular Forms
Angular ships two complementary approaches to building forms — template-driven forms for quick, simple cases and reactive forms for complex, testable, dynamically validated ones.
Two Ways to Build Forms
Template-driven forms let Angular infer the form model from directives placed directly in your HTML, driven by ngModel and the FormsModule. Reactive forms flip that around: you define the form model explicitly in TypeScript as a tree of FormControl and FormGroup instances, then bind your template to that model. Both styles produce the same runtime forms API underneath, so the choice comes down to how much control and testability you need.
Template-Driven Forms
To use template-driven forms, import FormsModule into your standalone component and attach ngModel to each input you want tracked. Angular automatically creates a NgForm instance on the <form> element (accessible via a template reference variable like #signupForm="ngForm") and a NgModel instance on each control, complete with validity state you can inspect directly in the template.
A template-driven signup form
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-signup-form',
standalone: true,
imports: [FormsModule],
template: `
<form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)">
<label>
Email
<input
name="email"
type="email"
required
email
ngModel
#emailField="ngModel"
/>
</label>
@if (emailField.invalid && emailField.touched) {
<p class="error">Please enter a valid email address.</p>
}
<label>
Password
<input name="password" type="password" required minlength="8" ngModel />
</label>
<button type="submit" [disabled]="signupForm.invalid">Sign up</button>
</form>
`
})
export class SignupFormComponent {
onSubmit(form: NgForm): void {
if (form.valid) {
console.log('Submitted value:', form.value);
}
}
}
bootstrapApplication(SignupFormComponent).catch((err) => console.error(err));
Reactive Forms
Reactive forms move the source of truth into TypeScript. You build the form with FormBuilder (or by hand with `new FormGroup(...)`), attach validators as plain functions, and bind the template with formGroup and formControlName. Because the model exists independently of the DOM, it's straightforward to unit test, patch values programmatically, or react to value changes with observables like valueChanges.
A reactive profile form
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>
Username
<input formControlName="username" />
</label>
@if (profileForm.get('username')?.invalid && profileForm.get('username')?.touched) {
<p class="error">Username must be at least 3 characters.</p>
}
<label>
Bio
<textarea formControlName="bio"></textarea>
</label>
<button type="submit" [disabled]="profileForm.invalid">Save profile</button>
</form>
`
})
export class ProfileFormComponent {
profileForm = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
bio: ['']
});
constructor(private fb: FormBuilder) {}
onSubmit(): void {
if (this.profileForm.valid) {
console.log('Profile saved:', this.profileForm.value);
}
}
}
bootstrapApplication(ProfileFormComponent).catch((err) => console.error(err));
- The form model lives in TypeScript, so it's testable without rendering a template
- Custom validators are plain functions, easy to unit test in isolation
- You can subscribe to valueChanges and statusChanges as observables
- Dynamic forms (adding or removing controls at runtime) are far simpler
- No two-way binding lag between keystrokes and the model
A custom validator with error messages
import { Component } from '@angular/core';
import {
ReactiveFormsModule,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors
} from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
const hasSpaces = /\s/.test(control.value ?? '');
return hasSpaces ? { noSpaces: true } : null;
}
@Component({
selector: 'app-account-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="accountForm" (ngSubmit)="onSubmit()">
<label>
Account handle
<input formControlName="handle" />
</label>
@if (accountForm.get('handle')?.hasError('required')) {
<p class="error">A handle is required.</p>
}
@if (accountForm.get('handle')?.hasError('noSpaces')) {
<p class="error">Handles cannot contain spaces.</p>
}
<button type="submit" [disabled]="accountForm.invalid">Create account</button>
</form>
`
})
export class AccountFormComponent {
accountForm = this.fb.group({
handle: ['', [Validators.required, noSpacesValidator]]
});
constructor(private fb: FormBuilder) {}
onSubmit(): void {
console.log(this.accountForm.value);
}
}
bootstrapApplication(AccountFormComponent).catch((err) => console.error(err));
Exercise: Angular Forms
What are the two main approaches to building Angular forms?