What Are Template-Driven Forms?
Template-driven forms rely on directives in the template to create and manage form controls. The logic lives primarily in the HTML template, making them simple and quick to build.
| Feature | Template-Driven | Reactive |
|---|---|---|
| Logic location | Template (HTML) | Component (TypeScript) |
| Data model | Two-way binding (ngModel) | FormGroup / FormControl |
| Validation | Template directives | Programmatic validators |
| Best for | Simple forms | Complex/dynamic forms |
| Testing | Harder to unit test | Easy to unit test |
Setup
Import FormsModule in your component:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-login-form',
standalone: true,
imports: [FormsModule],
templateUrl: './login-form.component.html',
})
export class LoginFormComponent {
// ...
}Basic Form
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [FormsModule],
template: `
<form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
<div>
<label for="name">Name</label>
<input id="name"
name="name"
[(ngModel)]="model.name"
required
minlength="2"
#name="ngModel">
@if (name.invalid && name.touched) {
<div class="error">
@if (name.errors?.['required']) {
<p>Name is required.</p>
}
@if (name.errors?.['minlength']) {
<p>Name must be at least 2 characters.</p>
}
</div>
}
</div>
<div>
<label for="email">Email</label>
<input id="email"
name="email"
type="email"
[(ngModel)]="model.email"
required
email
#emailField="ngModel">
@if (emailField.invalid && emailField.touched) {
<div class="error">
@if (emailField.errors?.['required']) {
<p>Email is required.</p>
}
@if (emailField.errors?.['email']) {
<p>Please enter a valid email.</p>
}
</div>
}
</div>
<div>
<label for="message">Message</label>
<textarea id="message"
name="message"
[(ngModel)]="model.message"
required
minlength="10"
#message="ngModel"></textarea>
@if (message.invalid && message.touched) {
<div class="error">Message must be at least 10 characters.</div>
}
</div>
<button type="submit" [disabled]="contactForm.invalid">
Send Message
</button>
</form>
`,
})
export class ContactFormComponent {
model = {
name: '',
email: '',
message: '',
};
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Form submitted:', this.model);
// Send to API
form.reset(); // Reset the form
}
}
}Key Concepts
ngModel — Two-Way Binding
<!-- Two-way binding -->
<input [(ngModel)]="user.name" name="name">
<!-- One-way binding (read from model) -->
<input [ngModel]="user.name" name="name">
<!-- Event binding (write to model) -->
<input (ngModelChange)="onNameChange($event)" name="name">Important: Every form control with ngModel must have a name attribute.
Template Reference Variables
<!-- #myField="ngModel" gives access to the control's state -->
<input name="email" ngModel required #emailRef="ngModel">
<p>Valid: {{ emailRef.valid }}</p>
<p>Touched: {{ emailRef.touched }}</p>
<p>Value: {{ emailRef.value }}</p>Form Reference
<form #myForm="ngForm" (ngSubmit)="submit(myForm)">
<!-- controls -->
</form>
<p>Form valid: {{ myForm.valid }}</p>
<p>Form dirty: {{ myForm.dirty }}</p>
<p>Form values: {{ myForm.value | json }}</p>Form Control States
Angular tracks the state of each control and the form:
| State | Class (true) | Class (false) | Meaning |
|---|---|---|---|
| Touched | .ng-touched | .ng-untouched | User has focused and blurred |
| Dirty | .ng-dirty | .ng-pristine | Value has been changed |
| Valid | .ng-valid | .ng-invalid | All validators pass |
Styling Based on State
input.ng-invalid.ng-touched {
border-color: red;
}
input.ng-valid.ng-touched {
border-color: green;
}
.error {
color: red;
font-size: 0.85em;
margin-top: 4px;
}Built-in Validators
| Directive | Purpose | Example |
|---|---|---|
required | Field must have a value | <input required> |
minlength | Minimum character count | <input minlength="3"> |
maxlength | Maximum character count | <input maxlength="100"> |
pattern | Regex pattern match | <input pattern="[a-zA-Z]+"> |
email | Valid email format | <input email> |
min | Minimum number value | <input type="number" min="0"> |
max | Maximum number value | <input type="number" max="100"> |
Different Input Types
Select Dropdown
<select name="country" [(ngModel)]="model.country" required>
<option value="" disabled>Select a country</option>
@for (country of countries; track country.code) {
<option [value]="country.code">{{ country.name }}</option>
}
</select>Radio Buttons
<div>
<label>
<input type="radio" name="plan" [(ngModel)]="model.plan" value="free">
Free
</label>
<label>
<input type="radio" name="plan" [(ngModel)]="model.plan" value="pro">
Pro
</label>
<label>
<input type="radio" name="plan" [(ngModel)]="model.plan" value="enterprise">
Enterprise
</label>
</div>Checkbox
<label>
<input type="checkbox" name="terms" [(ngModel)]="model.agreeToTerms" required>
I agree to the terms and conditions
</label>Complete Registration Form
@Component({
selector: 'app-register',
standalone: true,
imports: [FormsModule],
template: `
<h2>Create Account</h2>
<form #regForm="ngForm" (ngSubmit)="register(regForm)">
<div class="form-group">
<label>Full Name</label>
<input name="fullName" [(ngModel)]="user.fullName"
required minlength="2" #fullName="ngModel">
@if (fullName.invalid && fullName.touched) {
<small class="error">Enter your full name (min 2 chars)</small>
}
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" [(ngModel)]="user.email"
required email #email="ngModel">
@if (email.invalid && email.touched) {
<small class="error">Valid email required</small>
}
</div>
<div class="form-group">
<label>Password</label>
<input name="password" type="password" [(ngModel)]="user.password"
required minlength="8" #password="ngModel">
@if (password.invalid && password.touched) {
<small class="error">Password must be at least 8 characters</small>
}
</div>
<div class="form-group">
<label>Role</label>
<select name="role" [(ngModel)]="user.role" required>
<option value="">Select role</option>
<option value="student">Student</option>
<option value="professional">Professional</option>
<option value="educator">Educator</option>
</select>
</div>
<label class="checkbox">
<input type="checkbox" name="terms" [(ngModel)]="user.agreeToTerms" required>
I agree to the Terms of Service
</label>
<button type="submit" [disabled]="regForm.invalid">
Create Account
</button>
<p class="form-status" *ngIf="submitted">
✅ Account created successfully!
</p>
</form>
`,
})
export class RegisterComponent {
user = {
fullName: '',
email: '',
password: '',
role: '',
agreeToTerms: false,
};
submitted = false;
register(form: NgForm) {
if (form.valid) {
console.log('Registration:', this.user);
this.submitted = true;
form.reset();
}
}
}When to Use Template-Driven Forms
✅ Simple forms (login, contact, search) ✅ Quick prototyping ✅ Forms with minimal validation ✅ When you prefer keeping logic in the template
❌ Complex dynamic forms (fields added/removed at runtime) ❌ Forms requiring advanced validation logic ❌ When you need extensive unit testing of form logic
Next Steps
For more complex requirements, Angular offers reactive forms — a powerful programmatic approach to building and validating forms entirely in TypeScript.