Chapter 9: Forms in Angular
Most applications need to collect user input — login pages, search boxes, registration forms, checkout screens. Angular offers two powerful ways to handle forms:
- Template-driven forms — simple, declarative, good for small forms.
- Reactive forms — explicit, programmatic, great for complex or dynamic forms.
Both approaches share the same goals:
- Capture user input.
- Track state (dirty, touched, valid).
- Validate input.
- React to changes.
In this chapter, we’ll explore both strategies in detail.
9.1 Template-Driven Forms
Template-driven forms use Angular’s directives like ngModel inside templates. Most of the logic lives in the HTML, and Angular builds the form model under the hood.
Example: Login Form (Template-Driven)
TypeScript (Component):
import { Component } from '@angular/core';
@Component({
selector: 'app-login',
standalone: true,
templateUrl: './login.component.html'
})
export class LoginComponent {
email: string = '';
password: string = '';
login() {
console.log('Email:', this.email);
console.log('Password:', this.password);
}
}
Template:
<form (ngSubmit)="login()">
<label>Email</label>
<input type="email" [(ngModel)]="email" name="email" required>
<label>Password</label>
<input type="password" [(ngModel)]="password" name="password" required>
<button type="submit">Login</button>
</form>
✅ Here:
[(ngModel)]binds input values to component properties.nameattribute is required for Angular to track each control.- Validation (
required) is handled via HTML5 + Angular’s directives.
9.2 Validation in Template-Driven Forms
Angular automatically applies CSS classes to inputs:
ng-valid/ng-invalidng-touched/ng-untouchedng-dirty/ng-pristine
Example:
<input type="email" [(ngModel)]="email" name="email" required>
<div *ngIf="emailControl.invalid && emailControl.touched">
Email is required!
</div>
You can also use Angular’s #ref="ngModel" to access state:
<input type="email" [(ngModel)]="email" name="email" required #emailCtrl="ngModel">
<div *ngIf="emailCtrl.invalid && emailCtrl.touched">
Invalid email!
</div>
9.3 Reactive Forms
Reactive forms are more programmatic. Instead of Angular inferring the form model, you explicitly create it using FormGroup, FormControl, and FormBuilder.
Example: Registration Form (Reactive)
TypeScript (Component):
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './register.component.html'
})
export class RegisterComponent {
registerForm: FormGroup;
constructor(private fb: FormBuilder) {
this.registerForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
submit() {
if (this.registerForm.valid) {
console.log(this.registerForm.value);
}
}
}
Template:
<form [formGroup]="registerForm" (ngSubmit)="submit()">
<label>Name</label>
<input formControlName="name">
<div *ngIf="registerForm.get('name')?.invalid && registerForm.get('name')?.touched">
Name is required
</div>
<label>Email</label>
<input formControlName="email">
<div *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched">
Enter a valid email
</div>
<label>Password</label>
<input type="password" formControlName="password">
<div *ngIf="registerForm.get('password')?.invalid && registerForm.get('password')?.touched">
Password must be at least 6 characters
</div>
<button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>
9.4 Reactive vs Template-Driven
| Feature | Template-Driven | Reactive |
|---|---|---|
| Form model location | In the template (ngModel) | In the component class |
| Code style | Declarative, less boilerplate | Programmatic, more explicit |
| Validation | Built-in directives | Validators in code |
| Best for | Simple forms, quick prototypes | Complex forms, dynamic logic |
9.5 Advanced Reactive Features
9.5.1 FormArray
Dynamic arrays of controls. Example: adding multiple phone numbers.
phones: FormArray;
this.phones = this.fb.array([this.fb.control('')]);
9.5.2 Dynamic Form Controls
Create controls at runtime based on server config.
9.5.3 Async Validators
Check availability with backend (e.g., unique username).
Validators.composeAsync([this.userService.usernameTakenValidator()])
9.8 Forms with Signals
Signals make it easier to manage form state without constantly reaching into FormControl objects. Starting with Angular v17, you can bridge Reactive Forms and Signals using the new valueChanges signal utilities. This approach combines the predictability of signals with the structure of reactive forms.
9.8.1 Converting Form Controls to Signals
Angular provides a helper:
import { toSignal } from '@angular/core/rxjs-interop';
This converts an Observable (like FormControl.valueChanges) into a signal.
Example:
import { Component, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-signal-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="nameControl">
<p>You typed: {{ name() }}</p>
`
})
export class SignalFormComponent {
nameControl = new FormControl('');
name = toSignal(this.nameControl.valueChanges, { initialValue: '' });
}
Here:
toSignal()wraps thevalueChangesObservable.- The template can read the value simply by calling
name().
9.8.2 Direct Signal State Management
For very simple forms, you don’t even need FormControl. You can bind inputs directly to signals.
Example:
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-signal-login',
standalone: true,
imports: [FormsModule],
template: `
<form (ngSubmit)="submit()">
<label>Email</label>
<input [(ngModel)]="email()" (ngModelChange)="email.set($event)" name="email">
<label>Password</label>
<input type="password" [(ngModel)]="password()" (ngModelChange)="password.set($event)" name="password">
<button type="submit">Login</button>
</form>
<p>Email preview: {{ email() }}</p>
`
})
export class SignalLoginComponent {
email = signal('');
password = signal('');
submit() {
console.log('Email:', this.email());
console.log('Password:', this.password());
}
}
Here, signal is used directly as the form state — no FormControl or FormGroup needed. This works great for small forms.
9.8.3 Mixing Signals and Reactive Forms
You can combine both approaches: use Reactive Forms for validation and structure, but expose state as signals for easy reactivity.
@Component({
selector: 'app-register-signal',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="username" placeholder="Username">
<p *ngIf="usernameError()">❌ Username is required</p>
<button type="submit">Register</button>
</form>
`
})
export class RegisterSignalComponent {
form = this.fb.group({
username: ['', Validators.required]
});
username = toSignal(this.form.get('username')!.valueChanges, { initialValue: '' });
usernameError = computed(() => this.form.get('username')!.invalid && this.form.get('username')!.touched);
constructor(private fb: FormBuilder) {}
submit() {
console.log('Form value:', this.form.value);
}
}
✅ Here we use:
toSignal()to track the username’s value as a signal.computed()to derive validation state reactively.
9.8.4 When to Use Signal-Based Forms
- ✅ Use direct signals for very simple forms (like a login or search input).
- ✅ Use Reactive Forms +
toSignal()for complex forms where validation, dynamic controls, or async rules are required. - 🚫 Avoid replacing all forms with signals today — Angular’s form APIs are still primarily built around
FormControlandFormGroup, and signals complement them rather than replace them.
9.9 Chapter Summary
- Angular supports three styles of forms:
- Template-driven (simple, declarative).
- Reactive (structured, programmatic).
- Signal-based (lightweight, reactive, new in Angular 17).
- Signals integrate seamlessly with form controls using
toSignal(). - For small forms, signals can completely replace controls.
- For complex forms, signals work best on top of Reactive Forms.