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
inputandchangeDOM events, and how to read$eventsafely
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$eventis 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
- Make a "character counter" under a text area using
[(ngModel)]andname().length. - Change
ShoutingInputto also reject codes longer than 8 characters. - Add a button that sets the value of the manual input from code, and see that
[value]updates the field.
Quick check
- Which module do you import for
ngModel? - What is
$eventinside(ngModelChange)? - When does the
changeevent fire, compared withinput?
Next, continue with Angular Signals: Reactive State for Beginners.