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 checkedChangeA 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.