How-To

How-To · Signals

How to Set Up Two-Way Binding in Angular

Bind an input to a signal in Angular by reading it with [value] and writing it in the (input) event, so the field and the state always mirror each other — with the [(ngModel)] shorthand as the alternative.

XCODXHow-To · Updated

Overview

Two-way binding keeps an input and your state in lockstep: what the user types flows into state, and what is in state flows back into the field. With signals you build it from two one-way bindings — read the signal into the input’s value, and write it back on every change. The recommended approach is three small moves, and the [(ngModel)] shorthand is a fourth option once you have seen the parts.

Steps

1. Hold the value in a signal

Every bound field needs a piece of reactive state to live in. Declare it with signal(), giving it a sensible starting value — usually an empty string for a text field. This signal is now the single source of truth for what the field contains: the input will display it and update it, but the value itself lives here in state.

name = signal("");

2. Read the signal into the input with [value]

Bind the signal to the input by setting its value property to the signal read. This is the first half of two-way binding — the state-to-view direction. On the first render the field shows the empty string you initialised, and from then on the field always mirrors whatever the signal holds, because [value]="name()" re-renders whenever the signal changes.

<input [value]="name()" />

3. Write the input back into the signal in (input)

Now add the view-to-state direction. Listen for the (input) event and write the field’s current text into the signal with .set(), reading it from $event.target.value. Each keystroke fires the event, .set() stores the new value and notifies dependents, and the view stays in sync. Together, [value] reading out and (input) writing back are two-way binding built by hand.

<input
  [value]="name()"
  (input)="name.set($any($event.target).value)"
/>

Put together, the three moves live in one small standalone component: a signal holds the value, [value] reads it into the field, and (input) writes each keystroke back through .set().

import { Component, signal } from "@angular/core";

@Component({
  selector: "app-root",
  standalone: true,
  template: `
    <input [value]="name()" (input)="name.set($any($event.target).value)" />
    <p>You typed: {{ name() }}</p>
  `
})
export class App {
  name = signal("");
}

Runnable example

Here are the three steps together — an input bound to a signal, whose typed value is echoed live below the field. Type in it and watch the text appear as you go, which is the state-to-view and view-to-state directions both working at once:

The echo under the input reads straight from the signal, which is why it updates on every keystroke. Open it in the editor and try adding a second field; the same pattern extends to any number of inputs, each with its own signal.

Angular also offers a shorthand. Import FormsModule and you can collapse the read and the write into a single banana-in-a-box binding, [(ngModel)], on a plain property. It reads and writes in one expression, which is convenient for template-driven forms, though it works on a component property rather than directly on a signal:

Common pitfalls

One more note: read the typed value from $event.target.value in the (input) handler, not from the signal — the signal has not been updated yet at that moment. The event carries the newest text; you write it into the signal, and the signal then drives the view.

Frequently asked questions

How do I do two-way binding with signals in Angular?
Build it from two one-way bindings: read the signal into the input with [value]="name()", and write it back on change with (input)="name.set($event.target.value)". The field and the signal then always mirror each other.
What is the [(ngModel)] syntax in Angular?
It is the two-way binding shorthand, the banana-in-a-box syntax, that reads and writes a property in one expression: [(ngModel)]="name". It requires FormsModule in the component’s imports and binds to a plain property rather than directly to a signal.
Why is my Angular input not updating when I type?
You almost certainly bound [value] from a signal but forgot the (input) handler. With the value pinned to state and nothing writing back, the field is effectively read-only. Add (input)="name.set($event.target.value)".
Do I need FormsModule for two-way binding?
Only for the [(ngModel)] shorthand, which needs FormsModule in the standalone component’s imports. The signal approach — [value] plus (input) — needs no extra module, since it uses ordinary property and event bindings.