Template-driven forms use directives in templates to create and manipulate the underlying form object model. They rely on two-way data binding with ngModel to track changes and update the data model.
Template-driven forms are ideal for:
Simple forms with minimal validation
Forms that closely mirror the view structure
Quick prototypes and small applications
Scenarios where you prefer working in templates over component classes
For more complex scenarios, consider reactive forms .
Import FormsModule in your component or module:
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-contact' ,
standalone: true ,
imports: [ FormsModule ],
template: `...`
})
export class ContactComponent {}
Basic ngModel Usage
The ngModel directive creates a FormControl instance and binds it to a form control element.
One-Way Binding
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-name' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<label for="name">Name:</label>
<input id="name" type="text" [ngModel]="name">
<p>Hello, {{ name }}!</p>
`
})
export class NameComponent {
name = 'Nancy' ;
}
Two-Way Binding
Use banana-in-a-box syntax [(ngModel)] for two-way data binding:
packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-name-editor' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<label for="name">Name:</label>
<input id="name" type="text" [(ngModel)]="name">
<p>Hello, {{ name }}!</p>
`
})
export class NameEditorComponent {
name = 'Nancy' ;
}
When using ngModel within a <form> tag, provide a name attribute so the control can be registered with the parent form.
packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-contact-form' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
<div>
<label for="firstName">First Name:</label>
<input
id="firstName"
name="firstName"
type="text"
[(ngModel)]="contact.firstName"
required>
</div>
<div>
<label for="lastName">Last Name:</label>
<input
id="lastName"
name="lastName"
type="text"
[(ngModel)]="contact.lastName"
required>
</div>
<div>
<label for="email">Email:</label>
<input
id="email"
name="email"
type="email"
[(ngModel)]="contact.email"
required
email>
</div>
<button type="submit" [disabled]="contactForm.invalid">Submit</button>
</form>
<p>Form Valid: {{ contactForm.valid }}</p>
<p>Form Value: {{ contactForm.value | json }}</p>
`
})
export class ContactFormComponent {
contact = {
firstName: '' ,
lastName: '' ,
email: ''
};
onSubmit ( form : any ) {
console . log ( 'Form submitted:' , form . value );
console . log ( 'Model:' , this . contact );
}
}
You can export the form directive into a local template variable using #varName="ngForm":
< form #contactForm = "ngForm" >
<!-- Form controls -->
</ form >
< p > Form Status: {{ contactForm.status }} </ p >
< p > Form Valid: {{ contactForm.valid }} </ p >
< p > Form Pristine: {{ contactForm.pristine }} </ p >
< p > Form Touched: {{ contactForm.touched }} </ p >
Accessing Individual Controls
Export individual controls to access their state:
packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-email-input' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<label for="email">Email:</label>
<input
id="email"
name="email"
type="email"
[(ngModel)]="email"
#emailField="ngModel"
required
email>
<div *ngIf="emailField.invalid && emailField.touched">
<p *ngIf="emailField.errors?.['required']">Email is required</p>
<p *ngIf="emailField.errors?.['email']">Invalid email format</p>
</div>
<p>Valid: {{ emailField.valid }}</p>
<p>Dirty: {{ emailField.dirty }}</p>
<p>Touched: {{ emailField.touched }}</p>
`
})
export class EmailInputComponent {
email = '' ;
}
Template-driven forms use directives for validation:
< form #userForm = "ngForm" >
< div >
< label for = "username" > Username: </ label >
< input
id = "username"
name = "username"
type = "text"
[(ngModel)] = "username"
#usernameField = "ngModel"
required
minlength = "3"
maxlength = "20"
pattern = "[a-zA-Z0-9]+" >
< div *ngIf = "usernameField.invalid && usernameField.touched" >
< p *ngIf = "usernameField.errors?.['required']" > Username is required </ p >
< p *ngIf = "usernameField.errors?.['minlength']" >
Username must be at least 3 characters
</ p >
< p *ngIf = "usernameField.errors?.['maxlength']" >
Username cannot exceed 20 characters
</ p >
< p *ngIf = "usernameField.errors?.['pattern']" >
Username can only contain letters and numbers
</ p >
</ div >
</ div >
< div >
< label for = "age" > Age: </ label >
< input
id = "age"
name = "age"
type = "number"
[(ngModel)] = "age"
#ageField = "ngModel"
required
min = "18"
max = "100" >
< div *ngIf = "ageField.invalid && ageField.touched" >
< p *ngIf = "ageField.errors?.['required']" > Age is required </ p >
< p *ngIf = "ageField.errors?.['min']" > Must be at least 18 </ p >
< p *ngIf = "ageField.errors?.['max']" > Must be 100 or less </ p >
</ div >
</ div >
</ form >
Built-in Validation Directives
required Requires a non-empty value
email Validates email format
minlength Minimum length for strings/arrays
maxlength Maximum length for strings/arrays
min Minimum value for numbers
max Maximum value for numbers
pattern Validates against a regex pattern
Grouped Controls
Use ngModelGroup to create sub-groups within a form:
packages/forms/src/directives/ng_model_group.ts
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
import { CommonModule } from '@angular/common' ;
@ Component ({
selector: 'app-user-form' ,
standalone: true ,
imports: [ FormsModule , CommonModule ],
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<div ngModelGroup="name" #nameGroup="ngModelGroup">
<label for="first">First Name:</label>
<input
id="first"
name="first"
type="text"
[(ngModel)]="user.name.first"
required>
<label for="last">Last Name:</label>
<input
id="last"
name="last"
type="text"
[(ngModel)]="user.name.last"
required>
<p *ngIf="nameGroup.invalid && nameGroup.touched">
Name is incomplete
</p>
</div>
<div ngModelGroup="address" #addressGroup="ngModelGroup">
<label for="street">Street:</label>
<input
id="street"
name="street"
type="text"
[(ngModel)]="user.address.street">
<label for="city">City:</label>
<input
id="city"
name="city"
type="text"
[(ngModel)]="user.address.city">
<label for="state">State:</label>
<input
id="state"
name="state"
type="text"
[(ngModel)]="user.address.state">
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
<pre>{{ userForm.value | json }}</pre>
`
})
export class UserFormComponent {
user = {
name: {
first: '' ,
last: ''
},
address: {
street: '' ,
city: '' ,
state: ''
}
};
onSubmit () {
console . log ( 'User:' , this . user );
}
}
ngModelOptions
Customize how ngModel works with ngModelOptions:
Standalone Controls
Create a control that doesn’t register with the parent form:
< form >
< input name = "login" [(ngModel)] = "login" >
<!-- This checkbox is standalone and won't be in form value -->
< input
type = "checkbox"
[(ngModel)] = "showAdvanced"
[ngModelOptions] = "{standalone: true}" >
Show advanced options?
</ form >
<!-- form value: {login: ''} -->
Update On Blur or Submit
Control when the model updates:
< input
name = "description"
[(ngModel)] = "description"
[ngModelOptions] = "{updateOn: 'blur'}" >
< form [ngFormOptions] = "{updateOn: 'submit'}" >
<!-- All controls update on submit -->
< input name = "username" [(ngModel)] = "username" >
< input name = "email" [(ngModel)] = "email" >
< button type = "submit" > Submit </ button >
</ form >
Custom Name
Provide a name through options instead of the name attribute:
< form >
< my-custom-form-control
name = "Nancy"
[(ngModel)] = "value"
[ngModelOptions] = "{name: 'user'}" >
</ my-custom-form-control >
</ form >
<!-- form value: {user: ''} -->
Text Inputs
< input type = "text" name = "username" [(ngModel)] = "username" >
< input type = "email" name = "email" [(ngModel)] = "email" >
< input type = "password" name = "password" [(ngModel)] = "password" >
< textarea name = "description" [(ngModel)] = "description" ></ textarea >
Checkboxes
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-preferences' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<label>
<input type="checkbox" name="newsletter" [(ngModel)]="newsletter">
Subscribe to newsletter
</label>
<p>Newsletter: {{ newsletter }}</p>
`
})
export class PreferencesComponent {
newsletter = false ;
}
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
@ Component ({
selector: 'app-survey' ,
standalone: true ,
imports: [ FormsModule ],
template: `
<div>
<label>
<input type="radio" name="size" [(ngModel)]="size" value="small">
Small
</label>
<label>
<input type="radio" name="size" [(ngModel)]="size" value="medium">
Medium
</label>
<label>
<input type="radio" name="size" [(ngModel)]="size" value="large">
Large
</label>
</div>
<p>Selected size: {{ size }}</p>
`
})
export class SurveyComponent {
size = 'medium' ;
}
Select Dropdowns
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
import { CommonModule } from '@angular/common' ;
@ Component ({
selector: 'app-country-selector' ,
standalone: true ,
imports: [ FormsModule , CommonModule ],
template: `
<label for="country">Country:</label>
<select id="country" name="country" [(ngModel)]="selectedCountry">
<option [ngValue]="null" disabled>Select a country</option>
<option *ngFor="let country of countries" [ngValue]="country">
{{ country.name }}
</option>
</select>
<p>Selected: {{ selectedCountry?.name }}</p>
`
})
export class CountrySelectorComponent {
countries = [
{ code: 'US' , name: 'United States' },
{ code: 'CA' , name: 'Canada' },
{ code: 'MX' , name: 'Mexico' }
];
selectedCountry : any = null ;
}
Multiple Select
import { Component } from '@angular/core' ;
import { FormsModule } from '@angular/forms' ;
import { CommonModule } from '@angular/common' ;
@ Component ({
selector: 'app-skills-selector' ,
standalone: true ,
imports: [ FormsModule , CommonModule ],
template: `
<label for="skills">Skills:</label>
<select id="skills" name="skills" [(ngModel)]="selectedSkills" multiple>
<option *ngFor="let skill of skills" [ngValue]="skill">
{{ skill }}
</option>
</select>
<p>Selected: {{ selectedSkills | json }}</p>
`
})
export class SkillsSelectorComponent {
skills = [ 'JavaScript' , 'TypeScript' , 'Angular' , 'React' , 'Vue' ];
selectedSkills : string [] = [];
}
Aspect Template-Driven Reactive Setup FormsModule ReactiveFormsModule Form model creation Implicit (by directives) Explicit (in component) Data model Mutable Immutable Form validation Directives Functions Predictability Asynchronous Synchronous Testability Less testable More testable Scalability Limited Excellent
Template-driven forms are perfect for simple forms with basic validation.
Get forms up and running quickly without much setup.
Template-centric approach
If your team prefers working in templates rather than component classes.
Template-driven forms are similar to AngularJS forms, easing migration.
Best Practices
Always provide a name attribute when using ngModel within a <form> tag.
Use template reference variables to access form and control state in templates.
For complex forms with dynamic controls or extensive validation logic, consider reactive forms .
Next Steps
Form Validation Learn about validation in template-driven forms
Reactive Forms Explore reactive forms for more complex scenarios