In an Angular component, what does model() declare, and what can a parent template bind to it?
answer
- an input the child may write
- set() and update() on the child side
- a free output named after it
- banana-in-a-box from the parent
- signal instance, not its value
basics
~20 smodel() declares a writable signal input: the parent can bind a value in, the child can set() or update() it, and Angular adds a <name>Change output so a [( )] binding to a signal or plain property stays in sync.
solid answer
~40 s`model()` from `@angular/core` declares a **model input**: a `ModelSignal<T>` that is an input, a writable signal and an output at once. The child reads it by calling it, and unlike a read-only `input()` it can write it with `set()` or `update()`. Angular generates an output named `<name>Change` that emits each new value the child writes. A parent can bind it four ways: two-way with `[(value)]="volume"` where `volume` is a `WritableSignal` (pass the signal itself, not `volume()`) or a plain class property; one-way with `[value]`; or only listen with `(valueChange)`. `model(0)` gives a `ModelSignal<number>`; `model<string>()` includes `undefined`; `model.required<T>()` makes binding the input side mandatory.
code
ts · 24 linesimport {Component, model, signal} from '@angular/core';
@Component({
selector: 'app-toggle',
template: `<button (click)="on.update(v => !v)">{{ on() ? 'ON' : 'OFF' }}</button>`,
})
export class Toggle {
on = model(false); // input 'on' + output 'onChange'
}
@Component({
selector: 'app-settings',
imports: [Toggle],
template: `
<app-toggle [(on)]="darkMode" />
<app-toggle [(on)]="beta" />
<app-toggle (onChange)="audit($event)" />
`,
})
export class Settings {
darkMode = signal(false); // bound as the signal instance
beta = true; // plain property also works
audit(value: boolean) { console.log('toggled to', value); }
}go deeper
Remember the three roles: a model input can be bound in, written by the child with set or update, and it brings its own Change output.
Explain the four ways a parent binds it and why the two-way form passes the signal instance rather than its current value.
Say when a one-way binding to a model input is a trap, and how required models and the missing transform option shape an API.
Frame model inputs as a library-wide convention: which components own editable state, and how that keeps parent and child agreeing on one value.
## What model() is In Angular, a **model input** is declared by initialising a class field with `model()` (or `model.required()`), imported from `@angular/core`. The call must run in an injection context, which in practice means a field initialiser of a component or directive. It returns a **`ModelSignal<T>`**, and that one object plays three roles: - **An input.** The parent can bind a value into it, exactly like an `input()`. - **A writable signal.** Inside the child you read it with `value()` and write it with `value.set(x)` or `value.update(fn)`. A plain `input()` returns a read-only `InputSignal` with no `set`. - **An output.** Angular creates an output whose name is the input name plus `Change` (`value` gets `valueChange`), and it emits every new value the child writes. The input name is the class field name, unless you pass the `alias` option: `model(0, {alias: 'level'})` exposes `level` and `levelChange`. ## Declaring one The three forms and their types: | Declaration | Type | Notes | |---|---|---| | `model(0)` | `ModelSignal<number>` | initial value when the parent binds nothing | | `model<string>()` | `ModelSignal<string \| undefined>` | no initial value, so `undefined` is in the type | | `model.required<boolean>()` | `ModelSignal<boolean>` | the parent must bind the input side | `model()` accepts only two options, `alias` and `debugName`. Unlike `input()`, it has **no `transform`** option: a model input cannot coerce `""` to `true` with `booleanAttribute`, because a value that goes out through the change output must have the same type as the value that comes in. ## What a parent can bind A parent template has four choices, and they behave differently: 1. **Two-way to a writable signal**: `<app-slider [(value)]="volume" />` where `volume = signal(0)`. Note that the binding names the signal instance, `volume`, not a call `volume()`. Angular reads the signal to push its value down and calls `set()` on it when the child writes. Writing `[(value)]="volume()"` is rejected at compile time, because a two-way binding needs something it can assign to. 2. **Two-way to a plain property**: `<app-slider [(value)]="volume" />` where `volume = 0`. Angular assigns the emitted value to the property. 3. **One-way in**: `<app-slider [value]="volume" />`. The parent pushes values in; the child can still write its own copy, but nothing flows back, so the two sides can drift apart. 4. **Listen only**: `<app-slider (valueChange)="log($event)" />`. The parent reacts to the child's writes but does not feed a value in, so the child starts from its initial value. Option 1 is the idiomatic choice in current Angular when the parent's state is itself signal-based. Option 2 is common in code that still keeps plain fields. The expansion of `[( )]` into a property binding plus an event binding is template syntax, covered where two-way binding is taught; for a model input you only need to know that the event half is the generated `<name>Change` output. ## Reading the value Because a `ModelSignal` is a signal, reading it inside a template, `computed()` or `effect()` tracks it as a dependency. When the parent pushes a new value, or the child writes one, everything that read it is notified. That is also why a model input works with the `OnPush` default of Angular 22: a signal read in the template marks the view for refresh when the signal changes. ## A worked example ```ts import {Component, model, signal} from '@angular/core'; @Component({ selector: 'app-rating', template: ` @for (star of [1, 2, 3, 4, 5]; track star) { <button (click)="score.set(star)">{{ star <= score() ? '*' : '-' }}</button> } `, }) export class Rating { score = model(0); } @Component({ selector: 'app-review', imports: [Rating], template: `<app-rating [(score)]="stars" /> <p>You gave {{ stars() }}</p>`, }) export class Review { stars = signal(3); } ``` The rating starts at 3 because the parent pushes it down. Clicking the fifth button calls `score.set(5)` in the child; the child's own signal changes at once, `scoreChange` emits `5`, and the two-way binding sets `stars` to 5 in the parent. ## When to reach for it - A component whose job is to edit one value on the user's behalf: a toggle, a slider, a rating, a date picker, a collapsible section's open state. - When the parent wants to both seed the value and see every change the user makes. - Not for values the child only displays: those stay plain `input()`s, which keep the one-way data flow easy to reason about. ## Version notes `model()` was added in Angular 17.2 and is marked public API since 19.0. In Angular 22.2 it is the recommended way to author a two-way bindable component property; the older decorator pattern, an `@Input() value` paired with an `@Output() valueChange`, still supports the same `[(value)]` syntax.
- What does model.required() change compared with model()?`model.required<T>()` takes no initial value, so its type has no `undefined`, and the compiler reports an error if a parent uses the component without binding the input side. Reading it or calling `update()` before a value arrives throws a runtime error (NG0952), so read it in the template, a `computed()`, or `ngOnInit` and later, not in the constructor.
- Why does [(value)]="volume()" fail to compile when volume is a signal?A two-way binding must be able to write back, so its expression has to be an assignable property read. `volume()` is a call that produces a value, and nothing can be assigned to a value. Passing `volume`, the signal instance, lets Angular read it for the input side and call `set()` on it for the output side.
- Can you rename the public side of a model input?Yes: `model(0, {alias: 'level'})` exposes the input as `level` and the output as `levelChange`, while the class still uses the field name. As with inputs, aliasing is best kept for avoiding a clash with a native DOM property or preserving an old public name.
saying these in an interview costs you the question
- model() is just another name for input(); the child still cannot write it
- You must declare a separate output() for [( )] to work with model()
- The parent must bind volume() rather than the signal volume
- A model input can only be bound to a signal, never a plain property
- model() accepts a transform like input() does