Two-way binding synchronises a value between a component property and a form input — when the user types, the property updates; when the property changes programmatically, the input reflects it. Angular’s [(ngModel)] is the classic syntax. Under the hood, it is just [ngModel]="value" (property binding, model → view) and (ngModelChange)="value = $event" (event binding, view → model) combined. Angular 17.1+ introduced the model() signal function as a modern type-safe alternative for component-level two-way binding.
Two-Way Binding Forms
import { Component, signal, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
// ── Classic [(ngModel)] ────────────────────────────────────────────────────
@Component({
selector: 'app-search-input',
standalone: true,
imports: [FormsModule], // required for [(ngModel)]
template: `
<input type="text"
[(ngModel)]="searchTerm"
placeholder="Search...">
<p>Current search: {{ searchTerm }}</p>
`,
})
export class SearchInputComponent {
searchTerm = ''; // string property — ngModel binds to this
// Any change in the input field instantly updates searchTerm
// Any code that sets searchTerm updates the input field value
}
// ── Two-way binding desugared — equivalent to [(ngModel)] ──────────────────
// [(ngModel)]="searchTerm" is shorthand for:
// [ngModel]="searchTerm" (ngModelChange)="searchTerm = $event"
// ── Custom two-way binding on a component ─────────────────────────────────
// Parent: <app-rating [(value)]="postRating" />
// This requires: [value] @Input + (valueChange) @Output
@Component({
selector: 'app-rating',
standalone: true,
template: `
@for (star of stars; track star) {
<span (click)="setRating(star)"
[class.filled]="star <= value">★</span>
}
`,
})
export class RatingComponent {
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>(); // must be inputName + "Change"
stars = [1, 2, 3, 4, 5];
setRating(star: number): void {
this.valueChange.emit(star); // parent's value updates automatically
}
}
// ── Angular 17.1+ model() — signal-based two-way binding ──────────────────
@Component({
selector: 'app-rating-modern',
standalone: true,
template: `
@for (star of stars; track star) {
<span (click)="rating.set(star)"
[class.filled]="star <= rating()">★</span>
}
`,
})
export class RatingModernComponent {
rating = model(0); // signal that is also an @Input/@Output pair
// Parent: <app-rating-modern [(rating)]="postRating" />
// OR: <app-rating-modern [rating]="5" (ratingChange)="..." />
stars = [1, 2, 3, 4, 5];
}
FormsModule must be in the component’s imports: [] array (standalone) or the NgModule’s imports: [] to use [(ngModel)]. Without it, Angular reports “Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’.” This is one of the most common Angular beginner errors. For reactive forms (Chapter 55), you use ReactiveFormsModule instead. Never import both FormsModule and ReactiveFormsModule unless you genuinely need both form approaches in the same component.model() function (Angular 17.1+) creates a signal that doubles as a two-way bindable input. The parent binds with [(rating)]="myRating"; when the child calls this.rating.set(5), Angular automatically emits the new value to the parent through the binding. This is cleaner than the @Input() + @Output() nameChange pattern because it is a single declaration. Use model() for new components in Angular 18; use the classic pattern when supporting older Angular or when working with existing codebases.[(ngModel)] is appropriate for simple, flat forms (a few inputs, no complex validation). For forms with validation, nested objects, dynamic fields, or cross-field validation, use Angular Reactive Forms (Chapter 55) instead. Mixing ngModel with complex validation logic leads to messy component code that is hard to test. The rule: template-driven forms with ngModel for simple forms, reactive forms for anything complex.Common Mistakes
Mistake 1 — Forgetting FormsModule for [(ngModel)] (binding error)
❌ Wrong — [(ngModel)] used but FormsModule not in component’s imports: []; runtime error.
✅ Correct — add FormsModule to the component’s imports array alongside all other dependencies.
Mistake 2 — Custom two-way binding with mismatched Output name
❌ Wrong — @Input() value but @Output() changed — parent’s [(value)] does not connect.
✅ Correct — Output must be named exactly valueChange (input name + “Change”) for the [()] banana-in-a-box syntax to work.