ngModel Directive
ngModel is the directive that powers two-way data binding in Angular’s template-driven forms. It keeps a component property and a form control’s value in perfect sync: when the user types, the property updates, and when the property changes in code, the input reflects it. It lives in FormsModule, so you must import that module before the famous [(ngModel)] “banana-in-a-box” syntax will work.
How two-way binding works
The [(ngModel)] syntax is not magic — it is syntactic sugar that desugars into a property binding plus an event binding. These two lines are equivalent:
<!-- Banana in a box -->
<input [(ngModel)]="username" />
<!-- The expanded form Angular generates -->
<input [ngModel]="username" (ngModelChange)="username = $event" />
The [ngModel] half pushes the value into the input, and the (ngModelChange) half listens for changes and writes them back. Understanding this split is useful when you need to intercept a value before storing it.
Setting up FormsModule
In modern standalone Angular, you import FormsModule directly into the component that uses it — there is no NgModule required.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-greeting',
standalone: true,
imports: [FormsModule],
template: `
<label>
Your name:
<input [(ngModel)]="name" name="name" placeholder="Type here" />
</label>
<p>Hello, {{ name || 'stranger' }}!</p>
`,
})
export class GreetingComponent {
name = '';
}
As you type, the paragraph updates live. The name attribute is required whenever the control sits inside a <form> so Angular can register it with the form.
Forgetting to import
FormsModuleproduces the classic error: “Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’.” Add the import and the binding resolves.
Using ngModel with signals
[(ngModel)] binds to a plain property by assignment, so it does not write to a signal directly. Pair the expanded [ngModel] / (ngModelChange) form with a WritableSignal to keep your state reactive.
import { Component, signal, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [FormsModule],
template: `
<input
[ngModel]="query()"
(ngModelChange)="query.set($event)"
name="query"
placeholder="Search..."
/>
@if (query().length > 0) {
<p>Searching for "{{ query() }}" ({{ length() }} chars)</p>
}
`,
})
export class SearchComponent {
query = signal('');
length = computed(() => this.query().length);
}
Tracking control state and validation
Because ngModel creates an NgModel control instance, you can grab a template reference to it and read its validity and interaction state. The control exposes flags such as valid, invalid, pristine, dirty, touched, and untouched.
<form #form="ngForm">
<input
name="email"
type="email"
[(ngModel)]="email"
required
email
#emailCtrl="ngModel"
/>
@if (emailCtrl.invalid && emailCtrl.touched) {
<span class="error">
@if (emailCtrl.errors?.['required']) {
Email is required.
} @else if (emailCtrl.errors?.['email']) {
Enter a valid email address.
}
</span>
}
<button [disabled]="form.invalid">Submit</button>
</form>
Angular also adds CSS classes that mirror these flags, letting you style controls based on interaction:
| State class | Applied when |
|---|---|
ng-valid | The control passes all validators |
ng-invalid | The control fails a validator |
ng-pristine | The value has not changed yet |
ng-dirty | The value has been changed |
ng-touched | The control has been blurred |
ng-untouched | The control has not been blurred |
Controlling update timing
By default ngModel writes back on every keystroke. You can defer updates to the blur or submit event with the ngModelOptions input — handy for expensive change handlers.
<input
[(ngModel)]="comment"
name="comment"
[ngModelOptions]="{ updateOn: 'blur' }"
/>
Output (console while typing then blurring):
// nothing logs during typing
// on blur:
ngModelChange fired -> "Great article!"
updateOn accepts 'change' (default), 'blur', or 'submit'.
Best practices
- Always provide a
nameattribute onngModelcontrols inside a<form>so they register correctly. - Prefer the expanded
[ngModel]/(ngModelChange)pair when binding to signals or when you need to transform the value. - Reach for reactive forms instead of
ngModelfor complex, dynamic, or heavily validated forms — template-driven forms shine for simple cases. - Gate validation messages behind
touched(ordirty) so users are not scolded before they interact. - Use
[ngModelOptions]="{ updateOn: 'blur' }"to throttle costly change handlers rather than debouncing manually. - Avoid mixing
[(ngModel)]with reactiveformControlNameon the same control — it has been removed and throws an error.