Skip to content
Angular ng directives 3 min read

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 FormsModule produces 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 classApplied when
ng-validThe control passes all validators
ng-invalidThe control fails a validator
ng-pristineThe value has not changed yet
ng-dirtyThe value has been changed
ng-touchedThe control has been blurred
ng-untouchedThe 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 name attribute on ngModel controls 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 ngModel for complex, dynamic, or heavily validated forms — template-driven forms shine for simple cases.
  • Gate validation messages behind touched (or dirty) 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 reactive formControlName on the same control — it has been removed and throws an error.
Last updated June 14, 2026
Was this helpful?