Template-Driven Forms
By Syncfusion®, Inc. · Last updated
Building forms in the template with FormsModule and ngModel: every common input type, handling input, tracking form state, submitting and resetting, and best practices.
Introduction to Template-Driven Forms
A template-driven form is a form whose structure and data binding are written in the template. You add ngModel to each field, and Angular creates and tracks the form for you behind the scenes.
The class holds only the data, such as a user object. The template connects each input to a property of that data.
Real-World Example
A newsletter sign-up page has a name field, an email field, a topic dropdown and a "Send me offers" checkbox. Each field is connected to a property of a subscriber object in the class.
When the user clicks Subscribe, the class already holds every value the user entered, ready to send to the API. After a successful request, the form is cleared for the next sign-up.
NameEmailTopicOffers ☑subscriberobject in the classSubscribe(ngSubmit) → API[(ngModel)] keeps each field and its property in sync
Every field writes into one object. Submit sends that object.
FormsModule
FormsModule is the Angular package that provides template-driven forms. It contains the ngModel directive and the ngForm directive that Angular attaches to every <form> element.
Import it from @angular/forms and add it to the component's imports list:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-signup',
imports: [FormsModule],
templateUrl: './signup.html',
})
export class Signup {}
Without FormsModule, Angular does not recognise ngModel and reports NG0303: Can't bind to 'ngModel' since it isn't a known property of 'input'.
ngModel
ngModel is a directive that connects a form field to a property in the class. It also registers the field with the surrounding form, so Angular can track its value and state.
Inside a <form>, every field with ngModel needs a name attribute. Angular uses the name as the key of the field in the form's value.
<form>
<input name="email" [(ngModel)]="email" />
</form>
If a field inside a <form> has ngModel but no name, Angular reports: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.
Two-Way Data Binding with ngModel
[(ngModel)]="property" creates two-way binding between a field and a class property:
- When the user types, Angular writes the new value into the property.
- When code changes the property, Angular writes the new value into the field.
ngModel keeps the field and the property in sync
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<label>Project name: <input name="project" [(ngModel)]="projectName" /></label>
<p>Repository URL: github.com/acme/{{ projectName.toLowerCase().split(' ').join('-') }}</p>
<button (click)="projectName = 'Billing Service'">Use a suggested name</button>
`,
})
export class App {
projectName = 'Customer Portal';
}
Type in the field and the URL updates. Click the button and the field updates.
Form Controls
A form control is the object Angular creates for each field that has ngModel. It stores the field's current value and its state, such as whether the user has changed it.
The <form> element gets an NgForm object that groups all its controls. You can reach it with a template reference variable: #signupForm="ngForm".
| Object | Created for | Holds |
|---|---|---|
NgModel (a form control) | Each field with ngModel and name | One value and its state |
NgForm | Each <form> element | All controls, the combined value, the overall state |
NgForm collects every control into one value
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [FormsModule, JsonPipe],
template: `
<form #profileForm="ngForm">
<input name="firstName" [(ngModel)]="firstName" />
<input name="city" [(ngModel)]="city" />
</form>
<p>Controls: {{ controlNames(profileForm) }}</p>
<pre>{{ profileForm.value | json }}</pre>
`,
})
export class App {
firstName = 'Asha';
city = 'Pune';
controlNames(form: NgForm) {
return Object.keys(form.controls).join(', ');
}
}
Form Input Elements
ngModel works with every standard form element. The type of the value depends on the element.
| Element | Value type in the class |
|---|---|
| Text, email, password input, textarea | string |
| Number input | number |
| Date input | string in yyyy-mm-dd format |
| Checkbox | boolean |
| Radio buttons | The value of the selected button |
| Select dropdown | The value of the selected option, or the object bound with [ngValue] |
Text Input, Email Input and Password Input
type="text"accepts any text.type="email"shows an email keyboard on phones and enables email validation.type="password"hides the characters as the user types.
Textarea
<textarea> accepts text with several lines, such as comments or an address.
Number Input and Date Input
type="number"accepts only numbers. Angular converts the value to anumber.type="date"shows the browser's date picker. The value is a text inyyyy-mm-ddformat.
Select Dropdown
<select> lets the user pick one option. Use [value] for text values, or [ngValue] to store a number or a whole object.
Checkbox
A checkbox stores true when it is ticked and false when it is not.
Radio Button
Radio buttons with the same name form one group. Only one can be selected, and the property stores that button's value.
Every common input type bound with ngModel
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [FormsModule, JsonPipe],
template: `
<form class="grid">
<label>Name <input type="text" name="name" [(ngModel)]="model.name" /></label>
<label>Email <input type="email" name="email" [(ngModel)]="model.email" /></label>
<label>Password <input type="password" name="password" [(ngModel)]="model.password" /></label>
<label>Seats <input type="number" name="seats" [(ngModel)]="model.seats" min="1" /></label>
<label>Start date <input type="date" name="start" [(ngModel)]="model.start" /></label>
<label>Plan
<select name="plan" [(ngModel)]="model.plan">
@for (p of plans; track p) { <option [value]="p">{{ p }}</option> }
</select>
</label>
<label>Billing
<span><input type="radio" name="billing" value="monthly" [(ngModel)]="model.billing" /> Monthly
<input type="radio" name="billing" value="yearly" [(ngModel)]="model.billing" /> Yearly</span>
</label>
<label><input type="checkbox" name="terms" [(ngModel)]="model.terms" /> I accept the terms</label>
<label>Notes <textarea name="notes" rows="2" [(ngModel)]="model.notes"></textarea></label>
</form>
<pre>{{ model | json }}</pre>
`,
styles: `
.grid { display: grid; gap: 6px; max-width: 360px; }
label { display: flex; justify-content: space-between; gap: 8px; }
`,
})
export class App {
plans = ['Starter', 'Team', 'Enterprise'];
model = {
name: 'Asha',
email: 'asha@example.com',
password: 'secret123',
seats: 5,
start: '2026-04-01',
plan: 'Team',
billing: 'yearly',
terms: true,
notes: '',
};
}
Change any field and watch the JSON below update. seats stays a number, and terms stays a boolean.
File Input (Overview)
<input type="file"> lets the user choose files from their computer. ngModel does not work with file inputs, because the browser does not allow code to set a file input's value.
Instead, listen to the change event and read the selected files from the element:
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
this.fileName = file.name; // upload it with HttpClient later
}
}
<input type="file" (change)="onFileSelected($event)" />
Uploading the file to a server is covered in the HTTP chapter.
Handling User Input
ngModel updates the property on every key press. To run your own code when the value changes, split the binding into [ngModel] and (ngModelChange).
(ngModelChange) fires with the new value each time the user changes the field.
ngModelChange runs code on every change
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<label>Coupon: <input name="coupon" [ngModel]="coupon" (ngModelChange)="onCouponChange($event)" /></label>
<p>Stored as: {{ coupon }}</p>
<p>{{ message }}</p>
`,
})
export class App {
coupon = '';
message = 'Try SAVE10';
onCouponChange(value: string) {
this.coupon = value.toUpperCase().trim();
this.message = this.coupon === 'SAVE10' ? '✅ 10% discount applied' : 'Code not recognised yet';
}
}
The handler stores the code in upper case, so save10 and SAVE10 are treated the same.
Handling Multiple Inputs
When a form has many fields, keep their values in one object instead of separate properties. Bind each field to a property of that object: [(ngModel)]="address.city".
One object is easier to validate, reset and send to an API in a single request.
Several fields bound to one address object
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Address {
street: string;
city: string;
pin: string;
}
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<form>
<p><input name="street" placeholder="Street" [(ngModel)]="address.street" /></p>
<p><input name="city" placeholder="City" [(ngModel)]="address.city" />
<input name="pin" placeholder="PIN code" [(ngModel)]="address.pin" /></p>
</form>
<p>Ship to: {{ address.street }}, {{ address.city }} - {{ address.pin }}</p>
`,
})
export class App {
address: Address = { street: '12 MG Road', city: 'Bengaluru', pin: '560001' };
}
Form State Tracking
Angular tracks the state of every control and of the whole form. Each state is a true/false property:
| Property | true when |
|---|---|
pristine | The user has not changed the value yet |
dirty | The user has changed the value |
untouched | The user has not left the field yet |
touched | The user focused the field and then left it |
valid | The value passes all validation rules |
invalid | The value fails at least one rule |
Angular also adds matching CSS classes to each field, such as ng-dirty, ng-touched and ng-invalid. Style invalid fields with a selector such as input.ng-invalid.ng-touched, so the red border appears only after the user has visited the field.
Form and field state, updated as you interact
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<form #f="ngForm">
<input name="company" [(ngModel)]="company" #companyField="ngModel" required />
</form>
<p>Field: {{ companyField.dirty ? 'dirty' : 'pristine' }}, {{ companyField.touched ? 'touched' : 'untouched' }}, {{ companyField.valid ? 'valid' : 'invalid' }}</p>
<p>Form: {{ f.valid ? 'valid' : 'invalid' }}</p>
`,
styles: `input.ng-invalid.ng-touched { border: 2px solid #dc2626; }`,
})
export class App {
company = 'Acme Ltd';
}
Click in the field, delete the text and click outside. The field becomes dirty, touched and invalid, and it gets a red border.
Form Submission
Listen to (ngSubmit) on the <form> element to handle submission. ngSubmit runs when the user clicks a submit button or presses Enter in a field, and it prevents the browser from reloading the page.
Put (ngSubmit) on the <form> element and give the button type="submit". Then both a click and the Enter key submit the form, and the page never reloads. Disable the button while the form is invalid with [disabled]="form.invalid".
ngSubmit sends the form data without reloading the page
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<form #contactForm="ngForm" (ngSubmit)="send()">
<input name="email" type="email" [(ngModel)]="email" required />
<button type="submit" [disabled]="contactForm.invalid">Request a demo</button>
</form>
<p>{{ status }}</p>
`,
})
export class App {
email = 'ravi@example.com';
status = 'Not submitted';
send() {
this.status = 'Demo requested for ' + this.email;
}
}
Resetting Forms
form.reset() clears every field and returns every control to the pristine and untouched state. Bound properties become null.
To reset to specific values instead of empty fields, pass an object: form.reset({ email: 'default@example.com' }). To reset in code, read the form with @ViewChild('f') form!: NgForm;.
reset() clears the fields and the form state
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<form #feedbackForm="ngForm">
<input name="subject" [(ngModel)]="subject" />
<textarea name="message" [(ngModel)]="message" rows="2"></textarea>
</form>
<p>Subject: {{ subject || '(empty)' }} · Form is {{ feedbackForm.dirty ? 'dirty' : 'pristine' }}</p>
<button (click)="feedbackForm.reset()">Clear</button>
<button (click)="feedbackForm.reset({ subject: 'Bug report', message: '' })">Use template</button>
`,
})
export class App {
subject = 'Login page is slow';
message = 'It takes 8 seconds to load.';
}
Template-Driven Form Best Practices
- Give every field a
name. It is required inside a<form>, and it becomes the key inform.value. - Keep the data in one model object, such as
user, so it can be validated, reset and sent in one step. - Use
(ngSubmit), not(click), to submit. It also handles the Enter key and prevents a page reload. - Disable submit while the form is invalid with
[disabled]="form.invalid". - Match the input type to the data, such as
type="email"ortype="number", for better phone keyboards and correct value types. - Choose reactive forms for complex cases. Template-driven forms are best for small, simple forms. Dynamic fields and complex validation are easier with reactive forms, covered in the next lesson.
Common Mistakes
| Mistake | What you see | Fix |
|---|---|---|
FormsModule not imported | NG0303: Can't bind to 'ngModel' | Add FormsModule to the component's imports |
ngModel without name inside a form | the name attribute must be set error | Add a unique name to each field |
Two fields with the same name | Both fields show the same value | Give each field its own name |
[ngModel] without (ngModelChange) | Typing does not update the class | Use [(ngModel)] for two-way binding |
ngModel on a file input | The selected file is never stored | Read event.target.files in a (change) handler |
Submit button with (click) instead of (ngSubmit) | Pressing Enter does nothing, or the page reloads | Put (ngSubmit) on the <form> element |
Check Yourself
Exercises
What to Remember
- Template-driven forms need
FormsModule. Each field uses[(ngModel)]and aname. - Angular tracks each control's value and state (
dirty,touched,valid), and the whole form through#f="ngForm". - Submit with
(ngSubmit), disable the button while the form is invalid, and clear it withform.reset().
Next: Reactive Forms, how to define a form's structure and rules in the component class.