Template-Driven Forms
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Template-Driven Forms Companion Article
Template-driven forms are Angular's simpler approach to building forms. Most of the form structure and validation rules are declared directly inside the HTML template using directives such as ngModel, ngForm, required, minlength, and maxlength.
Angular automatically creates and manages the underlying form controls based on the directives used in the template. This makes template-driven forms convenient for login forms, contact forms, feedback forms, simple registration pages, search filters, and other forms that do not require highly complex state management.
For large forms with dynamic controls or complicated validation logic, Reactive Forms usually provide more explicit control. However, template-driven forms remain useful when the form structure is relatively straightforward.
Template-driven form features are provided by Angular's FormsModule.
Without importing FormsModule, directives such as ngModel and ngForm cannot be used.
In a standalone component, import it directly into the component:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [FormsModule],
templateUrl: './contact-form.component.html'
})
export class ContactFormComponent {
}
In applications that still use NgModules, FormsModule can be imported inside the required module.
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
FormsModule
]
})
export class AppModule {
}
Once FormsModule is available, Angular can automatically manage template-driven form controls.
Consider a simple user registration form.
export class RegisterComponent {
user = {
name: '',
email: ''
};
submitForm(): void {
console.log(this.user);
}
}
Template:
<form #registerForm="ngForm" (ngSubmit)="submitForm()">
<label for="name">Name</label>
<input
id="name"
type="text"
name="name"
[(ngModel)]="user.name">
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
[(ngModel)]="user.email">
<button type="submit">
Register
</button>
</form>
Angular handles the synchronization between the input elements and the component object.
ngModelngModel is one of the most important directives used in template-driven forms.
It connects an HTML form control with a property in the component.
For example:
<input
type="text"
name="username"
[(ngModel)]="username">
Component:
export class UserComponent {
username = '';
}
Whenever the user types into the input field, the username property is updated.
If the component changes the value of username, the input field is updated as well.
ngModelngModel can also be used with one-way binding.
<input
type="text"
name="username"
[ngModel]="username">
This sends the value from the component to the form control.
It does not automatically update the component when the user modifies the field.
ngModelChangeAngular exposes the ngModelChange event.
<input
type="text"
name="username"
[ngModel]="username"
(ngModelChange)="onUsernameChange($event)">
Component:
username = '';
onUsernameChange(value: string): void {
this.username = value;
console.log(value);
}
This approach is useful when additional logic needs to run whenever an input value changes.
The most common syntax in template-driven forms is:
[(ngModel)]="property"
This combines property binding and event binding.
Conceptually:
[ngModel] + (ngModelChange)
Example:
<input
type="text"
name="city"
[(ngModel)]="city">
Component:
city = 'Pune';
Initially, the input contains:
Pune
If the user changes the input to:
Mumbai
the component property automatically becomes:
city = 'Mumbai';
This synchronization is commonly called two-way data binding.
name Attribute Is ImportantControls registered with an ngForm normally need a name attribute.
Correct:
<input
type="text"
name="firstName"
[(ngModel)]="user.firstName">
Problematic:
<input
type="text"
[(ngModel)]="user.firstName">
The name identifies the control inside Angular's form model.
For example, with:
<input
name="email"
[(ngModel)]="user.email">
Angular may represent the control inside the form as:
email
Each registered control should therefore have a unique name within the form.
ngFormWhen FormsModule is imported, Angular automatically attaches an NgForm directive to standard <form> elements.
You can access this Angular form object using a template reference variable.
<form #userForm="ngForm">
Here:
userForm
references Angular's NgForm instance.
It provides useful information such as:
Example:
<form #userForm="ngForm">
<input
type="text"
name="username"
ngModel>
<input
type="email"
name="email"
ngModel>
</form>
<pre>{{ userForm.value | json }}</pre>
If the user enters:
username: Rahul
email: rahul@example.com
the form value can look like:
{
"username": "Rahul",
"email": "rahul@example.com"
}
Angular template-driven forms normally use the ngSubmit event.
<form
#loginForm="ngForm"
(ngSubmit)="onSubmit(loginForm)">
<input
type="email"
name="email"
ngModel>
<input
type="password"
name="password"
ngModel>
<button type="submit">
Login
</button>
</form>
Component:
import { NgForm } from '@angular/forms';
onSubmit(form: NgForm): void {
console.log(form.value);
}
The NgForm object gives the component access to both form values and form state.
If fields are already connected to a component object using two-way binding, passing the form object is not always necessary.
user = {
email: '',
password: ''
};
onSubmit(): void {
console.log(this.user);
}
Template:
<form (ngSubmit)="onSubmit()">
<input
type="email"
name="email"
[(ngModel)]="user.email">
<input
type="password"
name="password"
[(ngModel)]="user.password">
<button type="submit">
Login
</button>
</form>
Both approaches are valid. The best choice depends on whether the application needs direct access to Angular's form metadata.
Validation ensures that users enter acceptable data before it is processed.
Template-driven forms can use familiar HTML validation attributes, while Angular integrates those rules into its form state.
Example:
<input
type="text"
name="username"
[(ngModel)]="user.username"
required
minlength="3"
maxlength="20">
Angular monitors these rules and updates the validity of the corresponding form control.
Angular template-driven forms support commonly used validation rules.
Important examples include:
requiredminlengthmaxlengthpatternemail directive<input
type="text"
name="name"
ngModel
required>
The control is invalid while it contains no acceptable value.
<input
type="email"
name="email"
ngModel
required
email>
The field must contain a value accepted by Angular's email validator.
<input
type="password"
name="password"
ngModel
minlength="8">
The value must satisfy the configured minimum length.
<input
type="text"
name="username"
ngModel
maxlength="20">
This limits or validates the permitted length according to the applied form behavior.
A pattern can be used when data must match a specific format.
<input
type="text"
name="mobile"
ngModel
pattern="[0-9]{10}">
This example expects ten numeric digits.
A template reference variable can expose the NgModel directive for a particular control.
<input
type="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email>
You can then inspect properties such as:
<p>Valid: {{ email.valid }}</p>
<p>Invalid: {{ email.invalid }}</p>
<p>Touched: {{ email.touched }}</p>
<p>Dirty: {{ email.dirty }}</p>
This is especially useful for displaying validation messages.
A good form should explain why a value is invalid rather than simply preventing submission.
Example:
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email>
<div *ngIf="email.invalid && email.touched">
<p *ngIf="email.errors?.['required']">
Email is required.
</p>
<p *ngIf="email.errors?.['email']">
Enter a valid email address.
</p>
</div>
This approach avoids showing errors immediately when the page first loads.
The validation message appears after the user interacts with the field.
Applications using Angular's built-in control-flow syntax can express the same logic with @if.
<input
type="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email>
@if (email.invalid && email.touched) {
@if (email.errors?.['required']) {
<p>Email is required.</p>
}
@if (email.errors?.['email']) {
<p>Enter a valid email address.</p>
}
}
The validation behavior is the same. Only the template control-flow syntax differs.
The complete form exposes validity information through NgForm.
<form #userForm="ngForm">
<input
type="text"
name="name"
ngModel
required>
<button
type="submit"
[disabled]="userForm.invalid">
Submit
</button>
</form>
The button remains disabled until the form becomes valid.
This is a common pattern, although server-side validation must still be performed because browser-side validation alone cannot protect application data.
Angular automatically tracks user interaction with form controls.
The most important states are:
| State | Meaning |
|---|---|
valid | All current validation rules pass |
invalid | At least one validation rule fails |
pristine | The user has not changed the value |
dirty | The user has changed the value |
untouched | The control has not been blurred after interaction |
touched | The user has interacted with and left the control |
submitted | The form has been submitted |
These states help developers decide when validation feedback should be displayed.
A control is initially considered pristine.
pristine = true
dirty = false
After the user changes the value:
pristine = false
dirty = true
Example:
<p>Dirty: {{ username.dirty }}</p>
<p>Pristine: {{ username.pristine }}</p>
Before the user interacts with a field:
untouched = true
touched = false
After the field receives focus and is subsequently left:
untouched = false
touched = true
This makes touched particularly useful for validation messages.
@if (username.invalid && username.touched) {
<p>Please enter a valid username.</p>
}
Angular automatically adds CSS classes to form controls based on their state.
Common classes include:
ng-valid
ng-invalid
ng-pristine
ng-dirty
ng-untouched
ng-touched
These classes can be used to visually highlight validation status.
Example:
input.ng-invalid.ng-touched {
border: 1px solid red;
}
input.ng-valid.ng-touched {
border: 1px solid green;
}
This lets Angular's existing form state drive the UI without manually adding CSS classes.
Built-in validators cannot cover every business requirement.
Suppose a username must not contain spaces. A custom validator directive can enforce this rule.
Example:
import { Directive } from '@angular/core';
import {
AbstractControl,
NG_VALIDATORS,
ValidationErrors,
Validator
} from '@angular/forms';
@Directive({
selector: '[noSpaces]',
standalone: true,
providers: [
{
provide: NG_VALIDATORS,
useExisting: NoSpacesValidatorDirective,
multi: true
}
]
})
export class NoSpacesValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) {
return null;
}
return value.includes(' ')
? { noSpaces: true }
: null;
}
}
Use it in the template:
<input
type="text"
name="username"
[(ngModel)]="user.username"
#username="ngModel"
required
noSpaces>
Validation message:
@if (username.errors?.['noSpaces'] && username.touched) {
<p>Username cannot contain spaces.</p>
}
The validator returns:
null
when the value is valid.
When the value is invalid, it returns a validation error object:
{
noSpaces: true
}
Sometimes validation depends on multiple controls.
Examples include:
Template-driven forms can support more advanced validation through custom directives attached to the form or a group of controls.
However, when validation logic becomes highly interconnected, Reactive Forms often provide a clearer implementation because the form model is explicitly defined in TypeScript.
Template-driven forms can be reset using the resetForm() method.
<form
#userForm="ngForm"
(ngSubmit)="onSubmit(userForm)">
<input
type="text"
name="name"
[(ngModel)]="user.name">
<button type="submit">
Save
</button>
<button
type="button"
(click)="userForm.resetForm()">
Reset
</button>
</form>
Calling:
userForm.resetForm();
resets the form's controls and state.
States such as dirty and touched are also reset.
A form can also be reset with specific values.
form.resetForm({
name: 'Guest',
email: ''
});
This is useful when the application wants to restore predefined defaults instead of clearing every field.
When [(ngModel)] is connected to an object, developers may also want to restore that object.
Example:
user = {
name: '',
email: ''
};
resetForm(form: NgForm): void {
this.user = {
name: '',
email: ''
};
form.resetForm(this.user);
}
Keeping the component model and form state synchronized prevents confusing UI behavior after a reset.
Component:
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [FormsModule],
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
user = {
name: '',
email: '',
age: null as number | null
};
onSubmit(form: NgForm): void {
if (form.invalid) {
return;
}
console.log('Submitted Data:', this.user);
}
reset(form: NgForm): void {
this.user = {
name: '',
email: '',
age: null
};
form.resetForm(this.user);
}
}
Template:
<form
#profileForm="ngForm"
(ngSubmit)="onSubmit(profileForm)">
<div>
<label for="name">Name</label>
<input
id="name"
type="text"
name="name"
[(ngModel)]="user.name"
#name="ngModel"
required
minlength="3">
@if (name.invalid && name.touched) {
@if (name.errors?.['required']) {
<p>Name is required.</p>
}
@if (name.errors?.['minlength']) {
<p>Name must contain at least 3 characters.</p>
}
}
</div>
<div>
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email>
@if (email.invalid && email.touched) {
@if (email.errors?.['required']) {
<p>Email is required.</p>
}
@if (email.errors?.['email']) {
<p>Enter a valid email address.</p>
}
}
</div>
<div>
<label for="age">Age</label>
<input
id="age"
type="number"
name="age"
[(ngModel)]="user.age"
#age="ngModel"
required
min="18">
@if (age.invalid && age.touched) {
<p>Enter a valid age of 18 or above.</p>
}
</div>
<button
type="submit"
[disabled]="profileForm.invalid">
Save Profile
</button>
<button
type="button"
(click)="reset(profileForm)">
Reset
</button>
</form>
This example demonstrates the main pieces of a template-driven form:
FormsModulengFormngModelTemplate-driven forms work with <select> elements as well.
roles = [
'Developer',
'Tester',
'Designer'
];
selectedRole = '';
Template:
<select
name="role"
[(ngModel)]="selectedRole"
required>
<option value="">
Select Role
</option>
@for (role of roles; track role) {
<option [value]="role">
{{ role }}
</option>
}
</select>
The selected option is synchronized with the component property.
Radio buttons can share the same model.
<label>
<input
type="radio"
name="experience"
value="fresher"
[(ngModel)]="experience">
Fresher
</label>
<label>
<input
type="radio"
name="experience"
value="experienced"
[(ngModel)]="experience">
Experienced
</label>
Component:
experience = 'fresher';
Only one radio button in the group can be selected.
Checkboxes are commonly bound to boolean properties.
<label>
<input
type="checkbox"
name="acceptedTerms"
[(ngModel)]="acceptedTerms">
I accept the terms and conditions.
</label>
Component:
acceptedTerms = false;
When selected:
acceptedTerms === true
When cleared:
acceptedTerms === false
Related template-driven controls can be grouped using ngModelGroup.
Example:
<form #userForm="ngForm">
<div ngModelGroup="address">
<input
type="text"
name="city"
ngModel>
<input
type="text"
name="state"
ngModel>
</div>
</form>
The resulting value can have a nested structure:
{
"address": {
"city": "Pune",
"state": "Maharashtra"
}
}
This can make larger forms easier to organize.
ngModelSometimes an input appears inside a form but should not be registered as part of the Angular form.
You can configure ngModel as standalone.
<input
[(ngModel)]="showAdvanced"
[ngModelOptions]="{ standalone: true }"
type="checkbox">
This is useful for UI-only controls that should not become part of the submitted form model.
By default, form values are generally updated as the user changes the field.
ngModelOptions can change the update strategy.
Example:
<input
type="text"
name="username"
[(ngModel)]="username"
[ngModelOptions]="{ updateOn: 'blur' }">
With this configuration, the model update is associated with the blur interaction rather than every ordinary value-change interaction.
This can be useful when validation or processing should not run for every keystroke.
A control can also be configured to update when the form is submitted.
<input
type="text"
name="username"
[(ngModel)]="username"
[ngModelOptions]="{ updateOn: 'submit' }">
This behavior can reduce unnecessary updates when intermediate values are not needed.
When implementing template-driven forms, several practical rules improve reliability.
Even when the submit button is disabled, verify form validity in the submission handler.
onSubmit(form: NgForm): void {
if (form.invalid) {
return;
}
// Process valid form data
}
Angular validation improves user experience but does not replace backend validation.
A user can bypass browser-side checks and send requests directly to an API.
The server should independently validate:
When an API request is being processed, disabling the submit button can help prevent users from accidentally submitting the same form multiple times.
Prefer:
Password must contain at least 8 characters.
instead of:
Invalid input.
Specific feedback helps users correct problems faster.
FormsModuleIf FormsModule is missing, Angular will not recognize template-driven form directives correctly.
Make sure it is imported wherever the template requires these features.
name AttributeIncorrect:
<form>
<input [(ngModel)]="user.email">
</form>
Preferred:
<form>
<input
name="email"
[(ngModel)]="user.email">
</form>
Registered template-driven controls need a meaningful name.
This can create a poor user experience:
@if (email.invalid) {
<p>Invalid email.</p>
}
The message can appear before the user even interacts with the form.
A better condition is:
@if (email.invalid && email.touched) {
<p>Enter a valid email.</p>
}
Avoid submitting data without checking the form state.
onSubmit(form: NgForm): void {
if (form.invalid) {
return;
}
// Continue
}
Templates should remain readable.
Avoid expressions containing complicated validation or transformation logic.
Move complex logic into:
Both approaches use Angular's forms infrastructure, but they organize the form model differently.
| Template-Driven Forms | Reactive Forms |
|---|---|
| Form definition is mainly in the template | Form model is explicitly created in TypeScript |
Uses ngModel extensively | Uses FormControl, FormGroup, FormArray, and related APIs |
| Less setup for simple forms | More explicit setup |
| Convenient for small forms | Better suited to complex forms |
| Validation rules often appear in HTML | Validation logic is commonly configured in TypeScript |
| Angular creates much of the form model automatically | Developer explicitly constructs the form model |
| Easy for straightforward CRUD screens | Strong fit for dynamic and highly testable form logic |
Neither approach is universally better.
The choice should depend on the form's complexity and application requirements.
Template-driven forms work particularly well for:
They are especially convenient when:
The number of controls is small
+
Validation is straightforward
+
Form structure rarely changes dynamically
Consider Reactive Forms when the application requires:
Choosing the appropriate form architecture early can significantly reduce future maintenance complexity.
Suppose a registration form contains:
Name
Email
Password
Terms Accepted
A practical validation sequence can be:
User opens form
↓
All controls are initially pristine
↓
User interacts with Name
↓
Control becomes touched
↓
Angular checks required/minlength
↓
Error message appears if invalid
↓
User fixes the value
↓
Control becomes valid
↓
All controls become valid
↓
Submit button becomes available
↓
ngSubmit executes
↓
Component validates form again
↓
Data is sent to backend
Understanding this lifecycle makes template-driven form behavior easier to debug.
A template-driven form is an Angular form approach where most of the form definition and validation rules are declared in the HTML template. Angular creates and manages the underlying form model automatically.
FormsModule provides the main directives required for template-driven forms.
ngModel do?ngModel connects a form control to Angular's form model and can also synchronize the control value with a component property.
ngForm?ngForm represents the Angular form and exposes its value, controls, validation status, interaction state, and submission state.
name required with ngModel?The name identifies a control when Angular registers it inside the parent form.
dirty and touched?dirty indicates that the control value has changed.
touched indicates that the user has interacted with the control and moved focus away from it.
Validation messages are commonly shown when a control is both invalid and touched.
@if (email.invalid && email.touched) {
<p>Enter a valid email.</p>
}
Use:
form.resetForm();
or pass replacement values:
form.resetForm(initialData);
Yes. Custom validator directives can participate in Angular's validation system and return custom validation errors.
FormsModule enables template-driven form functionality.ngModel connects controls with Angular's form system.[(ngModel)] provides convenient two-way binding.ngForm represents the complete form.name.valid, invalid, dirty, pristine, touched, and untouched states.resetForm() resets values and form state.After studying template-driven forms, you should be able to:
FormsModule.ngForm.ngModel.ngSubmit.ngModelGroup.