Chapter 9: Forms – Template-driven & Reactive
Forms – Template-driven & Reactive — one of the most practical and frequently used parts of Angular.
In 2026 (with Angular ~21+), forms are in a transitional phase:
- Template-driven → still fully supported, simple for quick forms, and now works beautifully with signals via [(ngModel)] + signal()
- Reactive → remains the go-to for complex, validation-heavy, dynamic, or testable forms (enterprise loves it)
- Signal Forms (experimental / early stable in v21+) → a new third way that unifies everything with signals, reduces boilerplate, and is the future direction (but not yet replacing the classics for most production apps)
As your teacher, I’ll teach you what’s practical today (early 2026):
- Template-driven with signals (super clean for 70% of forms)
- Classic reactive forms (still king for complex needs)
- How signals integrate / bridge the two (using model(), toSignal(), etc.)
- Quick look at where Signal Forms are heading (so you’re ready when it matures)
We’ll build the same form twice: a registration form (name, email, password, age, terms checkbox).
1. Template-driven Forms + Signals (Modern, Recommended for Simple/Medium Forms)
Why love it in 2026?
- Minimal code in TS
- Two-way binding with [(ngModel)] + signal() or model()
- Validation via directives (required, email, minlength, etc.)
- Signals make the model reactive → UI auto-updates
Setup
Import FormsModule (not ReactiveFormsModule):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// app.component.ts or standalone component import { Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-registration', standalone: true, imports: [FormsModule], templateUrl: './registration.component.html', }) export class RegistrationComponent { // Use signal for the whole model (or separate signals) formData = signal({ name: '', email: '', password: '', age: null as number | null, terms: false }); // Or individual signals (sometimes cleaner) name = signal(''); email = signal(''); password = signal(''); age = signal<number | null>(null); terms = signal(false); submitted = signal(false); onSubmit() { this.submitted.set(true); if (this.isValid()) { console.log('Form submitted:', this.formData()); // In real app: call service / API } } private isValid() { const d = this.formData(); return d.name.trim() && d.email.includes('@') && d.password.length >= 8 && d.age != null && d.age >= 18 && d.terms; } } |
Template (registration.component.html)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<form (ngSubmit)="onSubmit()" #regForm="ngForm"> <h2>Register</h2> <div> <label>Name:</label> <input type="text" [(ngModel)]="name" name="name" required minlength="2" /> @if (submitted() && name().length < 2) { <span class="error">Name must be at least 2 characters</span> } </div> <div> <label>Email:</label> <input type="email" [(ngModel)]="email" name="email" required email /> @if (submitted() && !email().includes('@')) { <span class="error">Valid email required</span> } </div> <div> <label>Password:</label> <input type="password" [(ngModel)]="password" name="password" required minlength="8" /> @if (submitted() && password().length < 8) { <span class="error">Password min 8 chars</span> } </div> <div> <label>Age:</label> <input type="number" [(ngModel)]="age" name="age" required min="18" /> @if (submitted() && (age() == null || age() < 18)) { <span class="error">Must be 18+</span> } </div> <div> <label> <input type="checkbox" [(ngModel)]="terms" name="terms" required /> I accept terms </label> @if (submitted() && !terms()) { <span class="error">You must accept terms</span> } </div> <button type="submit" [disabled]="!regForm.valid || submitted()">Register</button> </form> @if (submitted()) { <pre>Submitted data: {{ formData() | json }}</pre> } |
Style tip (add to css):
|
0 1 2 3 4 5 6 |
.error { color: red; font-size: 0.9em; } |
Key modern points
- [(ngModel)]=”name” → two-way bind to signal → read with name() in TS
- Validation messages with @if + submitted() signal
- Form-level validity via #regForm=”ngForm” + regForm.valid
- No FormGroup boilerplate
2. Reactive Forms (Classic + Still Essential for Complex Cases)
Why still use?
- Dynamic fields (add/remove controls)
- Cross-field validation
- Easier unit testing
- Granular control over async validation, statusChanges, etc.
Setup
Import ReactiveFormsModule:
|
0 1 2 3 4 5 6 |
imports: [ReactiveFormsModule], |
Component TS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import { Component, inject } from '@angular/core'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; @Component({...}) export class RegistrationReactiveComponent { private fb = inject(FormBuilder); registrationForm: FormGroup = this.fb.group({ name: ['', [Validators.required, Validators.minLength(2)]], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], age: [null, [Validators.required, Validators.min(18)]], terms: [false, Validators.requiredTrue] }); submitted = false; get name() { return this.registrationForm.get('name'); } // ... similar getters for others onSubmit() { this.submitted = true; if (this.registrationForm.valid) { console.log('Submitted:', this.registrationForm.value); // API call } } } |
Template
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()"> <div> <label>Name:</label> <input formControlName="name" /> @if (submitted && name?.hasError('required')) { <span class="error">Required</span> } @if (submitted && name?.hasError('minlength')) { <span class="error">Min 2 chars</span> } </div> <!-- Similar for email, password, age, terms --> <button type="submit" [disabled]="registrationForm.invalid || submitted">Register</button> </form> |
Bridge to signals (very common hybrid)
|
0 1 2 3 4 5 6 7 8 9 10 |
// Add to component formValue = toSignal(this.registrationForm.valueChanges, { initialValue: this.registrationForm.value }); // Then in template use {{ formValue()?.name }} // Or computed(() => formValue()?.name?.toUpperCase()) |
3. Signals + Forms Integration Summary (2026 Best Practices)
| Scenario | Recommended Approach | Why / How Signals Fit In |
|---|---|---|
| Simple form (login, search) | Template-driven + [(ngModel)] + signal() | Zero boilerplate, two-way sync automatic, validation via directives + @if |
| Medium form | Template-driven + model() for two-way | age = model<number>() → banana-in-box [(age)] in child components too |
| Complex / dynamic / testable | Reactive Forms + toSignal(valueChanges) | Keep FormGroup, expose signals for template reactivity (e.g. isDirty = toSignal(form.statusChanges.pipe(map(s => s === ‘INVALID’))) |
| Future / new projects | Try Signal Forms (experimental) | form(modelSignal, schema) → unified signals API, auto validation signals, less code |
Quick Signal Forms sneak peek (experimental in v21+ — check angular.dev/guide/forms/signals)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
loginModel = signal({ email: '', password: '' }); loginForm = form(this.loginModel, schema => { required(schema.email); email(schema.email); required(schema.password); }); |
→ Then bind with <input [formField]=”loginForm.email” />
But for production in early 2026 → stick to the two classics + signal bridges.
Mini Practice Task
- Build the registration form with template-driven + signals (use individual signals)
- Add a child component <app-password-strength [password]=”password”></app-password-strength> that shows strength bar (use input() + computed())
- Switch to reactive version → add async validator for email uniqueness (fake delay)
- Bonus: Use model() in a custom input component for two-way binding
Forms are huge — but once you pick one style + signals, it clicks fast.
Next is usually advanced topics (defer, SSR, state management). Want to go there, or deepen forms (custom validators, async validation, Signal Forms deep dive, form arrays, testing forms)? Tell me — we’re building real apps now! 😄
