Template-Driven Forms
Template-driven forms let you build a form mostly in HTML: add ngModel to inputs, a few validation attributes, and Angular tracks values and validity for you.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Bind fields with ngModel and name
- Use #f="ngForm" to read form state
- Validate with required, minlength, email and pattern
- Submit with (ngSubmit) and reset
Explanation#
Import FormsModule. Every <form> automatically becomes an NgForm; each input with ngModel and a name becomes a control in it.
#f="ngForm" gives you the form in the template: f.valid, f.value, f.reset(). #email="ngModel" gives you one field: email.invalid, email.touched, email.errors.
Standard HTML validation attributes become Angular validators: required, minlength, maxlength, email, pattern, min, max.
Show errors when a field is invalid and touched (the user has left it) — or after a submit attempt (f.submitted).
(ngSubmit)="save(f)" handles submission without a page reload. Template-driven forms suit simple forms; for complex, dynamic or heavily tested forms prefer reactive forms.
Syntax#
<form #f="ngForm" (ngSubmit)="save(f)">
<input name="email" ngModel required email #email="ngModel" />
@if (email.invalid && email.touched) { <small>Enter a valid email</small> }
<button [disabled]="f.invalid">Send</button>
</form>
Code example: A contact form with validation messages#
import { Component, signal } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-contact-form',
imports: [FormsModule, JsonPipe],
template: `
<form #f="ngForm" (ngSubmit)="send(f)" novalidate>
<label>Name
<input name="name" ngModel required minlength="2" #name="ngModel" />
</label>
@if (name.invalid && (name.touched || f.submitted)) {
<small class="error">
@if (name.errors?.['required']) { Name is required. }
@if (name.errors?.['minlength']) { At least 2 characters. }
</small>
}
<label>Email
<input name="email" type="email" ngModel required email #email="ngModel" />
</label>
@if (email.invalid && (email.touched || f.submitted)) {
<small class="error">Enter a valid email address.</small>
}
<label>Topic
<select name="topic" [ngModel]="'Order'">
<option>Order</option><option>Refund</option><option>Other</option>
</select>
</label>
<textarea name="message" ngModel required maxlength="200"></textarea>
<button type="submit">Send</button>
</form>
@if (sent(); as data) { <pre>Sent: {{ data | json }}</pre> }
`,
styles: `.error { color: #dc2626; display: block; } label { display: block; margin: 6px 0; }`,
})
export class ContactForm {
sent = signal<unknown>(null);
send(form: NgForm) {
if (form.invalid) return; // errors are shown because f.submitted is now true
this.sent.set(form.value);
form.resetForm({ topic: 'Order' });
}
}
Clicking Send on the empty form shows “Name is required.” and “Enter a valid email address.”. Typing a one-letter name shows “At least 2 characters.” after leaving the field. A valid submission displays the values, e.g. Sent: { "name": "Asha", "email": "asha@example.com", "topic": "Refund", "message": "Where is my order?" }, and resets the form.This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Each input has a name and ngModel, so it registers as a control in the form.
- #name="ngModel" exposes the field’s state; errors?.["required"] tells which rule failed.
- Errors appear after the field is touched or after a submit attempt (f.submitted).
- [ngModel]="'Order'" sets a default without two-way binding.
- send() ignores invalid forms, stores the values and calls resetForm with defaults.
Real-world use case#
A “Contact us” page, a newsletter signup and a simple feedback form are quick to build as template-driven forms, with validation entirely in the markup.
Common mistakes#
An input with ngModel but no name inside a form throws an error and does not register.
Errors on untouched fields greet users with red text. Check touched or submitted.
A disabled button with no messages leaves users guessing. Show which fields are wrong.
Best practices#
- Use novalidate and Angular’s validation messages
- Show errors on touched or submitted
- Keep template-driven forms small
- Validate again on the server
Remember this#
ngModel + name = a control. #f="ngForm" = the whole form.
required, minlength, email, pattern work as validators.
Show errors when invalid && (touched || submitted).
Practice exercise#
Your task: Build a template-driven signup form with a username (required, pattern of letters and digits only) and show "Letters and numbers only" when the pattern fails.
Need a hint?
Show solution
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-signup',
imports: [FormsModule],
template: `
<form #f="ngForm">
<input name="username" ngModel required pattern="[A-Za-z0-9]+" #user="ngModel" />
@if (user.errors?.['pattern'] && user.touched) { <small>Letters and numbers only</small> }
<button [disabled]="f.invalid">Sign up</button>
</form>
`,
})
export class Signup {}
Mini quiz#
What must an input with ngModel inside a form also have?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.