Two-Way Binding
Two-way binding keeps a value in your component and a value in the template synchronized in both directions at once: when the model changes, the view updates, and when the user edits the view, the model updates. Angular expresses this with the distinctive [(...)] “banana-in-a-box” syntax. Under the hood there is no magic — it is simply a property binding and an event binding fused together, which means you can build your own two-way bindable properties on any component.
The banana-in-a-box syntax
The most common use of two-way binding is the NgModel directive on form inputs. Because NgModel lives in FormsModule, you must import it into a standalone component before the [(ngModel)] syntax works.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-name-editor',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name" placeholder="Your name" />
<p>Hello, {{ name() || 'stranger' }}!</p>
`,
})
export class NameEditorComponent {
name = signal('');
}
As you type into the <input>, name updates instantly, and because the same signal feeds the interpolation, the greeting re-renders on every keystroke. The brackets-and-parentheses are not a special operator — the brackets mark the value flowing into the element and the parentheses mark the change flowing out.
Tip: When binding
[(ngModel)]to a signal, Angular calls the signal as a setter for you — you write[(ngModel)]="name", not[(ngModel)]="name()". With a plain class field you would write the field name directly the same way.
How it desugars
[(prop)]="expr" is pure syntactic sugar. Angular expands it into a property binding plus an event binding by appending the suffix Change to the property name:
<!-- This banana-in-a-box... -->
<input [(ngModel)]="name" />
<!-- ...is exactly equivalent to this expanded form -->
<input [ngModel]="name" (ngModelChange)="name.set($event)" />
The pattern generalizes to any pair named foo and fooChange:
| Two-way form | Property binding (in) | Event binding (out) |
|---|---|---|
[(value)]="x" | [value]="x" | (valueChange)="x = $event" |
[(ngModel)]="x" | [ngModel]="x" | (ngModelChange)="x = $event" |
[(size)]="s" | [size]="s" | (sizeChange)="s = $event" |
Because it desugars, two-way binding only works when the target exposes both an input named prop and an output named propChange. If either half is missing, Angular reports a template error.
Custom two-way bindable properties
You can make any component participate in two-way binding by following the prop / propChange convention. The modern approach uses the model() signal function, which creates a writable signal that is simultaneously an input and an output — Angular wires up the Change output automatically.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="value.set(value() - 1)">-</button>
<span>{{ value() }}</span>
<button (click)="value.set(value() + 1)">+</button>
`,
})
export class CounterComponent {
// A two-way bindable model with a default of 0.
value = model(0);
}
A parent can now bind to it with the banana-in-a-box syntax, and reads stay in sync with the child’s writes:
import { Component, signal } from '@angular/core';
import { CounterComponent } from './counter.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CounterComponent],
template: `
<app-counter [(value)]="quantity" />
<p>Quantity selected: {{ quantity() }}</p>
`,
})
export class AppComponent {
quantity = signal(3);
}
Output:
[ - ] 3 [ + ]
Quantity selected: 3
Click + and both the child’s <span> and the parent’s paragraph jump to 4 — the change propagates outward through the generated valueChange output, and the parent’s signal is updated in place.
The classic decorator approach
Before model(), two-way bindings were built with a paired @Input() and @Output(). You will still see this in older codebases, and it remains valid:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `<button (click)="toggle()">{{ on ? 'On' : 'Off' }}</button>`,
})
export class ToggleComponent {
@Input() on = false;
@Output() onChange = new EventEmitter<boolean>();
toggle() {
this.on = !this.on;
this.onChange.emit(this.on); // emit on the `<prop>Change` output
}
}
Warning: The output’s name must be exactly the input name plus
Change(hereon+Change=onChange). A mismatch liketoggledwill silently break the two-way half of the binding even though both members compile fine on their own.
Best practices
- Reach for
model()in new code — it removes the boilerplate of a manual@Input()/@Output()pair and keeps everything signal-based. - Remember to import
FormsModulein any standalone component that uses[(ngModel)], or the template will fail to compile. - Keep two-way binding for genuinely bidirectional state (form fields, toggles, sliders); for one-directional data flow prefer a plain property binding plus an explicit event handler.
- Don’t put expressions with side effects inside
[(...)]— the desugaring evaluates the binding on every change, so keep it to a simple assignable target. - Name custom outputs as
propChangeexactly; a typo disables the two-way half without raising an error. - Avoid binding
[(ngModel)]inside a reactive form — use reactive form controls there instead to prevent conflicting state ownership.