Skip to content
Angular ng templates 4 min read

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 formProperty 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 (here on + Change = onChange). A mismatch like toggled will 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 FormsModule in 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 propChange exactly; 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.
Last updated June 14, 2026
Was this helpful?