Two-Way Binding in Angular
Two-way binding synchronises a component property and a template input in both directions at the same time. When the user changes the input the component property updates; when the component property changes the input reflects the new value. Angular's "banana-in-a-box" syntax [(ngModel)] is the most common form, but two-way binding extends to any component that follows the pattern.
The Banana-in-a-Box Syntax
The nickname comes from the way it looks: [( )] — square brackets inside parentheses. Conceptually it combines property binding [ ] (class → template) and event binding ( ) (template → class) into one.
<!-- Two-way binding --> [(ngModel)]="username" <!-- Equivalent long form --> [ngModel]="username" (ngModelChange)="username = $event"
Setting Up ngModel
ngModel lives in FormsModule. Import it in your standalone component or NgModule before using it.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [FormsModule], // <-- required
template: `
<label>
Name:
<input type="text" [(ngModel)]="name" />
</label>
<label>
Email:
<input type="email" [(ngModel)]="email" />
</label>
<p>Preview: {{ name }} <{{ email }}></p>
`,
})
export class ProfileFormComponent {
name = 'Alice';
email = 'alice@example.com';
}
Two-Way Binding with Different Input Types
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-input-types',
standalone: true,
imports: [FormsModule],
template: `
<!-- Text input -->
<input type="text" [(ngModel)]="text" placeholder="Text" />
<!-- Number input -->
<input type="number" [(ngModel)]="quantity" min="0" max="99" />
<!-- Checkbox -->
<input type="checkbox" [(ngModel)]="agreed" id="terms" />
<label for="terms">I agree to the terms</label>
<!-- Radio buttons -->
<input type="radio" [(ngModel)]="size" value="small" id="sm" /><label for="sm">Small</label>
<input type="radio" [(ngModel)]="size" value="medium" id="md" /><label for="md">Medium</label>
<input type="radio" [(ngModel)]="size" value="large" id="lg" /><label for="lg">Large</label>
<!-- Select / dropdown -->
<select [(ngModel)]="country">
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
</select>
<!-- Textarea -->
<textarea [(ngModel)]="notes" rows="4"></textarea>
<pre>{{ currentValues() }}</pre>
`,
})
export class InputTypesComponent {
text = 'Hello';
quantity = 1;
agreed = false;
size = 'medium';
country = 'ca';
notes = '';
currentValues() {
return JSON.stringify({
text: this.text,
quantity: this.quantity,
agreed: this.agreed,
size: this.size,
country: this.country,
}, null, 2);
}
}
Two-Way Binding Without FormsModule (Reactive Alternative)
In reactive forms you bind to the form control, not ngModel. This is the recommended approach for complex forms.
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-reactive-input',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="nameControl" />
<p>Value: {{ nameControl.value }}</p>
<p>Valid: {{ nameControl.valid }}</p>
`,
})
export class ReactiveInputComponent {
nameControl = new FormControl('Angular');
}
Two-Way Binding Between Components
Any component can support two-way binding if it follows the value/valueChange convention: expose an @Input() named x and an @Output() named xChange (same name + "Change" suffix). The parent can then use [(x)]="property".
// counter.component.ts (child)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="decrement()">-</button>
<span>{{ value }}</span>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
increment() { this.valueChange.emit(this.value + 1); }
decrement() { this.valueChange.emit(this.value - 1); }
}
// parent component
import { Component } from '@angular/core';
import { CounterComponent } from './counter.component';
@Component({
selector: 'app-shop',
standalone: true,
imports: [CounterComponent],
template: `
<h2>Shopping Cart</h2>
<app-counter [(value)]="cartQuantity" />
<p>You want {{ cartQuantity }} item(s)</p>
`,
})
export class ShopComponent {
cartQuantity = 1;
}
Two-Way Binding with Angular Signals (model())
Angular 17.1+ introduced model() — a signal-based alternative to the @Input / @Output + EventEmitter pattern. It makes two-way binding more ergonomic and pairs naturally with the Signals API.
// toggle.component.ts — using model()
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `
<button
[class.on]="checked()"
(click)="checked.set(!checked())"
>
{{ checked() ? 'ON' : 'OFF' }}
</button>
`,
})
export class ToggleComponent {
checked = model(false); // writable signal + @Input + @Output in one
}
// parent using two-way binding with model()
@Component({
selector: 'app-settings',
standalone: true,
imports: [ToggleComponent],
template: `
<app-toggle [(checked)]="darkMode" />
<p>Dark mode: {{ darkMode() }}</p>
`,
})
export class SettingsComponent {
darkMode = signal(false);
}
Comparison: ngModel vs Reactive Forms vs model()
Approach | Module Required | Best For | Angular Version |
|---|---|---|---|
[(ngModel)] | FormsModule | Simple template-driven forms | All |
FormControl + formControl | ReactiveFormsModule | Complex forms with validation | All |
model() signal | None (standalone) | Component-to-component two-way binding | 17.1+ |
Common Pitfalls
Forgetting FormsModule — ngModel silently fails without it.
Mutating objects directly — use spread (...) or Object.assign() to trigger change detection.
Binding to computed values — [(ngModel)]="user.name + suffix" is read-only, not writeable.
Mixing ngModel and reactive forms — use one or the other, not both on the same input.
Using two-way binding for read-only display — prefer one-way [property] binding for display-only values.
Summary
[(ngModel)] is the classic two-way binding — combine [ngModel] + (ngModelChange) under the hood.
Import FormsModule in the component or NgModule before using ngModel.
Works on text, number, checkbox, radio, select, and textarea inputs.
Component-level two-way binding: pair @Input() x with @Output() xChange.
Angular 17.1+ model() signal is the modern signal-based two-way binding primitive.
For complex forms, prefer ReactiveFormsModule (FormControl / FormGroup).