Angular Two-Way Binding: ngModel, ngModelChange and Input Events

Angular Tutorial: Beginner to Advanced · 3. Components and templates · 4 min read · Updated

Property binding sends data into an element, and event binding sends events out. Two-way binding does both at once, which is perfect for form fields: when the user types, your data changes, and when your data changes, the field updates.

What you will learn

  • What [(ngModel)] really is
  • How to split it into [ngModel] and (ngModelChange) when you need extra logic
  • The difference between the input and change DOM events, and how to read $event safely

Two-way binding with ngModel

// src/app/binding/greeting.ts
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-greeting',
  imports: [FormsModule],
  template: `
    <label for="name">Your name</label>
    <input id="name" name="name" [(ngModel)]="name" />
    <p>Hello, {{ name() || 'stranger' }}!</p>
    <button type="button" (click)="name.set('')">Clear</button>
  `,
})
export class Greeting {
  protected readonly name = signal('');
}

[(ngModel)] is nicknamed the "banana in a box" because the parentheses (banana) sit inside the brackets (box). ngModel lives in FormsModule, so you must import it. You can bind it straight to a signal: typing updates the signal, and name.set('') clears the input.

What the banana really means

[(ngModel)]="name" is shorthand for two bindings:

<input [ngModel]="name" (ngModelChange)="name = $event" />
  • [ngModel] sends the value in.
  • (ngModelChange) fires whenever the user changes the value, and $event is the new value itself (a string here, not a DOM event).

Splitting it lets you add logic in the middle, such as changing the value before it is stored:

// src/app/binding/shouting-input.ts
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-shouting-input',
  imports: [FormsModule],
  template: `
    <label for="code">Coupon code</label>
    <input id="code" name="code" [ngModel]="code()" (ngModelChange)="onCode($event)" />
    <p>Stored value: {{ code() }}</p>
  `,
})
export class ShoutingInput {
  protected readonly code = signal('');

  protected onCode(value: string): void {
    // Keep only letters and digits, and store them in upper case.
    this.code.set(value.replace(/[^a-z0-9]/gi, '').toUpperCase());
  }
}

Without FormsModule: input and change events

You can also build two-way binding by hand, using only the DOM. This needs no import:

// src/app/binding/manual-input.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-manual-input',
  template: `
    <label for="city">City</label>
    <input id="city" [value]="city()" (input)="onInput($event)" (change)="onCommit($event)" />
    <p>Typing: {{ city() }}</p>
    <p>Committed: {{ committed() }}</p>
  `,
})
export class ManualInput {
  protected readonly city = signal('');
  protected readonly committed = signal('');

  protected onInput(event: Event): void {
    this.city.set((event.target as HTMLInputElement).value);
  }

  protected onCommit(event: Event): void {
    this.committed.set((event.target as HTMLInputElement).value);
  }
}
  • The `input` event fires on every keystroke.
  • The `change` event fires when the user finishes: pressing Enter or leaving the field.

Use input for live feedback such as search, and change when you only care about the final value.

Fixing: Property 'value' does not exist on type 'EventTarget'

This TypeScript error shows up when you write $event.target.value in a template or in code:

// Error: Property 'value' does not exist on type 'EventTarget | null'
onInput(event: Event) {
  console.log(event.target.value);
}

The target of a generic Event could be any element, so TypeScript does not know it has a value. Tell TypeScript what you expect:

const input = event.target as HTMLInputElement;
console.log(input.value);

Or avoid the problem entirely with a template reference variable (covered in a later lesson): <input #box (input)="onInput(box.value)" />.

Put it together

// src/app/app.ts
import { Component } from '@angular/core';
import { Greeting } from './binding/greeting';
import { ShoutingInput } from './binding/shouting-input';
import { ManualInput } from './binding/manual-input';

@Component({
  imports: [Greeting, ShoutingInput, ManualInput],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <h1>Two-way binding</h1>
  <app-greeting />
  <app-shouting-input />
  <app-manual-input />
</main>

Which one should I use?

  • `[(ngModel)]` for simple template-driven forms and quick demos.
  • `[ngModel]` plus `(ngModelChange)` when you need to transform or validate the value in code.
  • Reactive forms (formControlName) for anything bigger. They are easier to test.
  • `model()` for two-way binding between your own parent and child components (covered in the outputs lesson).

Try it yourself

  1. Make a "character counter" under a text area using [(ngModel)] and name().length.
  2. Change ShoutingInput to also reject codes longer than 8 characters.
  3. Add a button that sets the value of the manual input from code, and see that [value] updates the field.

Quick check

  1. Which module do you import for ngModel?
  2. What is $event inside (ngModelChange)?
  3. When does the change event fire, compared with input?

Next, continue with Angular Signals: Reactive State for Beginners.

Frequently asked questions

Why does ngModel need a name attribute inside a form tag?

Inside a form element, ngModel registers itself with the form using the name. Without it, Angular throws an error. Outside a form it is not required, but adding it is harmless.

Is ngModel deprecated?

No. It is still supported for template-driven forms. Angular is also developing signal-based forms, covered in the advanced stage.