Two-way binding with model()

Angular · Practical · 9 min

Custom inputs, toggles and pickers should be as easy to bind as native form controls.

Two-way binding combines a property binding and an event binding: [(value)]="x" is shorthand for [value]="x" (valueChange)="x = $event" (the "banana in a box" syntax).

The `model()` function creates both sides in one declaration:

checked = model(false);          // ModelSignal<boolean>
this.checked.set(true);          // updates locally AND emits checkedChange

A model is a writable signal inside the child. When the child calls set or update, Angular emits checkedChange and the parent's bound value updates. When the parent's value changes, the model receives it like an input.

In the parent, bind a writable signal directly: <app-toggle [(checked)]="darkMode" /> where darkMode = signal(false). Angular reads and writes the signal for you (note: no parentheses after darkMode). Binding a plain field also works.

model.required<T>() makes the binding mandatory. A model can also be used one-way: the parent can just pass [checked]="x" or just listen with (checkedChange).

Use model() for value-like components (rating, toggle, date picker, tabs' active index). For components that should integrate with Angular forms (validation, touched state), signal forms let custom controls implement a small interface built on model() too.

Example

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

@Component({
  selector: 'app-rating',
  template: `
    @for (n of stars; track n) {
      <button (click)="value.set(n)">{{ n <= value() ? '★' : '☆' }}</button>
    }`,
})
export class Rating {
  value = model(0);                  // two-way bindable as [(value)]
  protected stars = [1, 2, 3, 4, 5];
}

@Component({
  selector: 'app-review',
  imports: [Rating],
  template: `
    <app-rating [(value)]="score" />
    <p>Score: {{ score() }}</p>      <!-- updates when a star is clicked -->
    <button (click)="score.set(0)">Reset</button>`,
})
export class Review {
  protected score = signal(3);
}

Remember

  • [(x)] = [x] + (xChange)
  • model() declares a writable, two-way bindable signal input
  • Bind a writable signal directly: [(value)]="score" without calling it

Watch out for

  • Writing [(value)]="score()" with parentheses; two-way binding needs the signal itself, not its value.

Check yourself

  1. Inside a child, `open = model(false)` and the code calls this.open.set(true). What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/templates/two-way-binding