Docs
/
Angular
Chapter 8
08 — Template-Driven Forms
When to Use Template-Driven Forms
| Scenario | Use |
|---|---|
| Simple login/search form | ✅ Template-driven |
| Complex multi-step form | ❌ Use Reactive |
| Dynamic fields (add/remove) | ❌ Use Reactive |
| Heavy custom validation | ❌ Use Reactive |
| Quick prototype | ✅ Template-driven |
Setup
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
})
Basic Form with ngModel
@Component({
imports: [FormsModule],
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<div>
<label for="name">Name</label>
<input
id="name"
name="name"
[(ngModel)]="user.name"
required
minlength="2"
#nameField="ngModel"
>
@if (nameField.invalid && nameField.touched) {
<span class="error">
@if (nameField.errors?.['required']) { Name is required }
@if (nameField.errors?.['minlength']) { Min 2 characters }
</span>
}
</div>
<div>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
[(ngModel)]="user.email"
required
email
#emailField="ngModel"
>
@if (emailField.invalid && emailField.touched) {
<span class="error">Valid email required</span>
}
</div>
<div>
<label for="role">Role</label>
<select id="role" name="role" [(ngModel)]="user.role" required>
<option value="">Select role</option>
@for (role of roles; track role) {
<option [value]="role">{{ role | titlecase }}</option>
}
</select>
</div>
<div>
<label>
<input type="checkbox" name="agree" [(ngModel)]="user.agree" required>
I agree to terms
</label>
</div>
<button [disabled]="userForm.invalid">Submit</button>
</form>
`,
})
export class UserFormComponent {
user = {
name: '',
email: '',
role: '',
agree: false,
};
roles = ['user', 'admin', 'editor'];
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Form value:', form.value);
form.resetForm(); // Reset form + clear validation state
}
}
}
Built-in Validation Directives
| Directive | Validator |
|---|---|
required | Field must have a value |
email | Valid email format |
minlength="n" | Minimum length |
maxlength="n" | Maximum length |
pattern="regex" | Regex match |
min="n" | Minimum number |
max="n" | Maximum number |
CSS Classes Added by Angular
Angular automatically adds CSS classes based on control state:
| State | True Class | False Class |
|---|---|---|
| Visited | .ng-touched | .ng-untouched |
| Changed | .ng-dirty | .ng-pristine |
| Valid | .ng-valid | .ng-invalid |
// Show red border on touched + invalid fields
input.ng-invalid.ng-touched {
border-color: red;
}
input.ng-valid.ng-touched {
border-color: green;
}
Custom Validator Directive
import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
@Directive({
selector: '[appNoWhitespace]',
standalone: true,
providers: [
{ provide: NG_VALIDATORS, useExisting: NoWhitespaceDirective, multi: true },
],
})
export class NoWhitespaceDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
return control.value.trim().length === 0 ? { whitespace: true } : null;
}
}
<input name="username" [(ngModel)]="username" appNoWhitespace>
Grouped Fields with ngModelGroup
<form #f="ngForm">
<div ngModelGroup="address" #addressGroup="ngModelGroup">
<input name="street" [(ngModel)]="address.street" required>
<input name="city" [(ngModel)]="address.city" required>
<input name="zip" [(ngModel)]="address.zip" required pattern="^\d{5}$">
</div>
@if (addressGroup.invalid && addressGroup.touched) {
<span class="error">Please complete all address fields</span>
}
</form>
<!-- f.value → { address: { street, city, zip } } -->
Reactive vs Template-Driven — Side by Side
Template-Driven
<input name="email" [(ngModel)]="email" required email #e="ngModel">
@if (e.errors?.['required']) { <span>Required</span> }
Reactive
email = new FormControl('', [Validators.required, Validators.email]);
<input [formControl]="email">
@if (email.errors?.['required']) { <span>Required</span> }
Key Takeaways
- Template-driven forms use
FormsModule+[(ngModel)]+ validation directives - Every form control needs a
nameattribute — Angular uses it as the key - Use
#field="ngModel"to get a reference for error checking in templates - CSS classes (
.ng-invalid.ng-touched) are free — use them for styling - Use
ngModelGroupto group related fields (address, payment, etc.) - Prefer Reactive Forms for complex forms — template-driven forms don't scale well