Template-Driven Forms
Template-driven forms let you build forms primarily in the HTML template using Angular directives. Angular reads the template and automatically constructs the underlying form model — you write less TypeScript at the cost of less programmatic control.
They are ideal for straightforward forms: login, contact, basic settings.
Setup: Importing FormsModule
// Standalone component
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [FormsModule], // enables ngModel, ngForm, ngModelGroup
templateUrl: './login.component.html',
})
export class LoginComponent {}
// NgModule-based app — import once in the module
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [FormsModule],
})
export class AppModule {}Your First Template-Driven Form
<!-- login.component.html -->
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
<label for="email">Email</label>
<input
id="email"
type="email"
name="email" <!-- required for ngModel tracking -->
ngModel <!-- binds to Angular's form model -->
required
email
/>
<label for="password">Password</label>
<input
id="password"
type="password"
name="password"
ngModel
required
minlength="8"
/>
<button type="submit" [disabled]="loginForm.invalid">Log In</button>
</form>import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [FormsModule],
templateUrl: './login.component.html',
})
export class LoginComponent {
onSubmit(form: NgForm) {
if (form.valid) {
console.log(form.value); // { email: '...', password: '...' }
}
}
}name attribute is mandatory on every input that uses ngModel. Angular uses it as the key in the form's value object. Omitting it causes a runtime error.Two-Way Binding with ngModel
By default, ngModel creates a one-way binding that registers the field with the form. Add the banana-in-a-box syntax [(ngModel)] to also bind the value back to a component property.
// component.ts
export class ProfileComponent {
user = {
name: 'Alice',
email: 'alice@example.com',
};
}<!-- profile.component.html -->
<form #profileForm="ngForm" (ngSubmit)="onSubmit()">
<!-- Two-way binding — model stays in sync as user types -->
<input name="name" [(ngModel)]="user.name" required />
<input name="email" [(ngModel)]="user.email" required email />
<!-- Live preview powered by two-way binding -->
<p>Preview: {{ user.name }} <{{ user.email }}></p>
<button [disabled]="profileForm.invalid">Save</button>
</form>Template Reference Variables
Export the NgModel directive to a template variable to access a single control's state directly in the template.
<input
name="username"
ngModel
#usernameField="ngModel" <!-- export NgModel instance -->
required
minlength="3"
/>
<!-- Access state properties on the control -->
<div *ngIf="usernameField.invalid && usernameField.touched">
<span *ngIf="usernameField.errors?.['required']">Username is required.</span>
<span *ngIf="usernameField.errors?.['minlength']">
Minimum 3 characters.
(you typed {{ usernameField.errors?.['minlength'].actualLength }})
</span>
</div>
<!-- Visual feedback via CSS classes -->
<p>
Dirty: {{ usernameField.dirty }} |
Touched: {{ usernameField.touched }} |
Valid: {{ usernameField.valid }}
</p>Angular's Automatic CSS Classes
Angular adds and removes CSS classes on form controls automatically, making it easy to style validation states without extra logic.
Class | Applied when |
|---|---|
ng-valid | All validators pass |
ng-invalid | At least one validator fails |
ng-pristine | Value has not been changed |
ng-dirty | Value has been changed |
ng-untouched | Field has never been blurred |
ng-touched | Field has been blurred at least once |
ng-pending | An async validator is running |
/* styles.css — style inputs based on validation state */
input.ng-invalid.ng-touched {
border-color: red;
outline-color: red;
}
input.ng-valid.ng-dirty {
border-color: green;
}Grouping Controls with ngModelGroup
Use ngModelGroup to nest controls into sub-objects inside the form value.
<form #checkoutForm="ngForm" (ngSubmit)="onSubmit(checkoutForm.value)">
<fieldset ngModelGroup="shipping">
<input name="street" ngModel required />
<input name="city" ngModel required />
<input name="zip" ngModel required />
</fieldset>
<fieldset ngModelGroup="billing">
<input name="street" ngModel required />
<input name="city" ngModel required />
<input name="zip" ngModel required />
</fieldset>
<button>Place Order</button>
</form>
<!-- form.value will be:
{
shipping: { street: '', city: '', zip: '' },
billing: { street: '', city: '', zip: '' }
}
-->Setting and Resetting Values
Use the NgForm reference to programmatically set or reset form values.
import { Component, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [FormsModule],
template: `
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<input name="email" ngModel required email />
<input name="name" ngModel required />
<button type="submit">Submit</button>
<button type="button" (click)="reset(f)">Reset</button>
</form>
`,
})
export class EditUserComponent {
@ViewChild('f') form!: NgForm;
ngAfterViewInit() {
// Populate form after view initialises
setTimeout(() => {
this.form.setValue({
email: 'alice@example.com',
name: 'Alice',
});
});
}
onSubmit(form: NgForm) {
console.log(form.value);
}
reset(form: NgForm) {
form.reset(); // clear values + reset pristine/touched state
// or: form.resetForm({ email: '', name: '' }); to reset with defaults
}
}Select, Checkbox, and Radio Inputs
<!-- Select dropdown --> <select name="role" [(ngModel)]="user.role"> <option value="">-- Choose role --</option> <option value="admin">Admin</option> <option value="editor">Editor</option> <option value="viewer">Viewer</option> </select> <!-- Checkbox --> <input type="checkbox" name="agreeToTerms" [(ngModel)]="agreeToTerms" required /> <!-- Radio buttons — same name, different values --> <label> <input type="radio" name="gender" ngModel value="male" /> Male </label> <label> <input type="radio" name="gender" ngModel value="female" /> Female </label> <label> <input type="radio" name="gender" ngModel value="other" /> Other </label>
Dynamic Options with ngFor
export class OrderComponent {
sizes = ['Small', 'Medium', 'Large', 'Extra Large'];
order = { size: 'Medium' };
}<select name="size" [(ngModel)]="order.size">
<option *ngFor="let size of sizes" [value]="size">{{ size }}</option>
</select>Common Validation Directives
Directive / Attribute | Validates |
|---|---|
required | Value is non-empty |
Value matches email pattern | |
minlength="N" | String length >= N |
maxlength="N" | String length <= N |
min="N" | Number >= N |
max="N" | Number <= N |
pattern="regex" | Value matches the pattern |
Validator and providing itself under the NG_VALIDATORS token. This lets you use your custom logic just like a built-in attribute.Summary: Template-driven forms are quick to build and require minimal TypeScript. Use ngModel and ngModelGroup to bind controls, #ref="ngForm" to access the form object, and Angular's automatic CSS classes for visual feedback. For complex dynamic forms, graduated to reactive forms which offer more programmatic control.