Model Inputs
Two-way binding used to require a matching pair: an @Input() for the value coming down and an @Output() valueChange event going back up, wired by hand for every property. The model() function (stable since Angular 17.2) collapses that ceremony into a single writable signal. A model() is simultaneously a signal input, a signal output, and a mutable piece of local state — which is exactly what the [(value)] “banana-in-a-box” syntax needs. This page covers how to declare model inputs, read and write them, mark them required, alias them, and the gotchas to watch for.
What model() actually creates
Calling model() returns a ModelSignal<T>. That single object plays three roles at once:
- It is a writable signal, so you call
value()to read andvalue.set(...)/value.update(...)to write. - It is a signal input, so a parent can bind into it with
[value]="...". - It implicitly declares a matching output named
<field>Change, so the parent can listen with(valueChange)="..."— or combine both into[(value)].
Because the output is generated automatically, you never write valueChange = output<T>() yourself.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `
<button type="button" (click)="toggle()">
{{ checked() ? 'On' : 'Off' }}
</button>
`,
})
export class ToggleComponent {
// ModelSignal<boolean> with an initial value of false
checked = model(false);
toggle() {
this.checked.update((v) => !v);
}
}
The parent binds it two-way. When the user clicks, the child updates checked, which emits checkedChange, which flows back into the parent’s darkMode signal — no event handler boilerplate required.
<app-toggle [(checked)]="darkMode" />
Reading and writing inside the component
Unlike input(), which is strictly read-only inside the component, a model() is fully writable. This is the whole point: the component owns mutations to the value, and each mutation propagates to the parent.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-stepper',
standalone: true,
template: `
<button (click)="dec()" [disabled]="value() <= min()">-</button>
<span class="value">{{ value() }}</span>
<button (click)="inc()">+</button>
`,
})
export class StepperComponent {
value = model(0);
min = model(0);
inc() { this.value.update((v) => v + 1); }
dec() { this.value.update((v) => Math.max(this.min(), v - 1)); }
}
<app-stepper [(value)]="quantity" [min]="1" />
Here value is bound two-way while min is bound one-way — a model() does not force the parent to use [(...)]. The parent can bind [min] only and simply ignore the minChange event.
Required model inputs
When a two-way binding is mandatory, use model.required<T>(). It has no initial value and the compiler forces the parent to provide a binding.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-rating',
standalone: true,
template: `
@for (star of stars; track star) {
<button (click)="rating.set(star)">
{{ star <= rating() ? '★' : '☆' }}
</button>
}
`,
})
export class RatingComponent {
rating = model.required<number>();
protected stars = [1, 2, 3, 4, 5];
}
If a consumer forgets to bind it, the build fails rather than leaving an undefined value at runtime.
Output:
NG8008: Required input 'rating' from component RatingComponent must be specified.
Aliasing a model
Pass an alias to expose a public binding name that differs from the class field. The generated change event uses the alias too, so the alias becomes <alias>Change.
checkedState = model(false, { alias: 'checked' });
// parent binds [(checked)] and listens for (checkedChange)
Tip: Unlike
input(),model()does not accept atransformoption. The value must round-trip unchanged so the two-way contract holds — a transform would mean the value written back differs from the value read, breaking the binding.
Subscribing to changes
A model() is a signal, so the idiomatic way to react to changes is computed() or effect() rather than subscribing to the generated output. The output exists for template binding; inside the class, treat the model as state.
import { Component, model, effect } from '@angular/core';
@Component({ selector: 'app-search', standalone: true, template: `<input />` })
export class SearchComponent {
query = model('');
constructor() {
effect(() => {
console.log('query is now:', this.query());
});
}
}
model() vs input() vs output()
| Concern | input() | output() | model() |
|---|---|---|---|
| Direction | parent → child | child → parent | both |
| Writable in component | no (read-only) | n/a (emit only) | yes |
| Template syntax | [x] | (x) | [(x)], [x], or (xChange) |
| Returned type | InputSignal<T> | OutputEmitterRef<T> | ModelSignal<T> |
| Required variant | input.required<T>() | n/a | model.required<T>() |
Supports transform | yes | n/a | no |
Warning: Writing to a
model()inside acomputed()or during change detection can causeExpressionChangedAfterItHasBeenChecked-style issues. Mutate models in response to user events or withineffect(), not while deriving a value.
Best practices
- Reach for
model()only when the component genuinely owns and mutates a value the parent also cares about (form fields, toggles, sliders); for one-way notifications preferoutput(). - Prefer
model.required<T>()over an arbitrary default when “no value” is not a meaningful state — let the compiler enforce the binding. - React to model changes with
computed()/effect()inside the class; reserve the generated<field>Changeoutput for template bindings. - Use
.update()for derived mutations (v => v + 1) and.set()for absolute assignments so intent is clear. - Remember a
model()can be bound one-way ([value]) too — don’t force consumers into[(...)]when they only read. - Don’t try to coerce model values with
transform; if you need coercion, use a one-wayinput()plus a separateoutput().