model() creates a signal that is at the same time input and output. The component can write to it (set/update), and the write automatically reaches the parent through the nameChange event. This is the basis of the two-way [( )] syntax (“banana in a box”).
Example
import { Component, input, model } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `
<button role="switch" [attr.aria-checked]="checked()" (click)="checked.set(!checked())">
<span class="knob"></span>
</button>
{{ label() }}
`,
})
export class Toggle {
// model() = input + output „checkedChange”, in a single writable signal
readonly checked = model(false);
readonly label = input('');
}{
"notifications": false,
"darkMode": true,
"guests": 2,
"plan": "Free"
}model.required
@Component({ selector: 'app-stepper', /* ... */ })
export class Stepper {
readonly value = model.required<number>(); // the parent MUST give a value
readonly min = input(0);
readonly max = input(10);
increment() {
this.value.update((v) => Math.min(v + 1, this.max())); // write → notifies the parent
}
}model() or input + output?
// model(): the state “belongs” to both — the control changes it, the parent can set it
readonly selected = model<string>();
// input + output: the child only PROPOSES, the parent decides (it can refuse / validate / save to the server)
readonly value = input.required<number>();
readonly valueChange = output<number>();
// parent: (valueChange)="confirmAndSave($event)"- “Value” controls: toggle, slider, stepper, colour picker, date picker.
- UI state the parent may want to control: active tab, expanded panel, open dialog.
- The simple equivalent of
ngModelfor your own components.
- The parent must validate or confirm the change (e.g. saving to the server).
- Events that aren’t “my value changed”:
(save),(delete). - Fields in a Signal Forms form → implement
FormValueControl(see Custom controls) — still withmodel(), but integrated with validation.
What model() actually creates
A field checked = model(false) declares two things on the component: an input named checked and an output named checkedChange. The [(checked)] syntax is just shorthand for both. That is why the parent can choose: two-way, input only or output only.
export class Toggle {
readonly checked = model(false);
// What Angular sees on the component:
// input: checked
// output: checkedChange
}
<!-- the same two bindings, written four ways -->
<app-toggle [(checked)]="enabled" />
<app-toggle [checked]="enabled()" (checkedChange)="enabled.set($event)" />
<app-toggle [checked]="true" /> <!-- input only -->
<app-toggle (checkedChange)="log($event)" /> <!-- output only -->A detail that surprises people: if the parent binds a fixed value ([checked]="true"), the child can still change its value with set(). The change stays local until the parent sends a new value, which overwrites it. If you want the parent to be the single source of truth, bind a signal with [( )].
Objects and arrays in model()
Like any signal, model() only reports a change when it receives a new reference. If you change a field of the object in place, neither the parent nor the template finds out. Always write a copy, with update().
export class AddressEditor {
readonly address = model<Address>({ street: '', city: '' });
setCity(city: string) {
// ✗ mutation: nobody finds out, the parent keeps the old value
this.address().city = city;
// ✓ new reference: the template and the parent update
this.address.update((a) => ({ ...a, city }));
}
}The options of model()
model() has only two options: alias (the public name; the output becomes aliasChange) and debugName (for Angular DevTools). It has no transform like input(). If you need to transform the incoming value, for example a string from HTML into a number, use input() with transform plus a separate output().
Common pitfalls
[(checked)]="notifications()"— with parentheses — is not two-way: you bind a value, not the signal. Write[(checked)]="notifications".- A read-only signal in the parent (a
computed) in[( )]→ compile error. Two-way needs aWritableSignalor a plain property. - Two “owners” of the state: if both the parent and the child apply logic to the same value, it gets hard to follow. Decide who is the source of truth.
Cheat sheet
model<T>() // ModelSignal<T | undefined>
model(default) // ModelSignal<T>
model.required<T>()
m.set(v); m.update(fn); // writes locally + emits `${name}Change`
<app-x [(value)]="sig" /> // two-way on a signal
<app-x [value]="v" (valueChange)="onChange($event)" />