In Angular, when does the implicit <name>Change output of a model() input emit, and when does it stay silent?
answer
- who performed the write
- set and update on the child
- parent pushes are not echoed
- equality check before emitting
basics
~20 sThe Change output emits only when the child writes the model with set() or update() and the new value differs by the signal's equality check; a value the parent pushes in through the binding updates the signal without emitting.
solid answer
~40 sA `ModelSignal`'s `set()` first compares the new value with the current one using the signal's equality function (`Object.is` by default). If they differ it stores the value and emits it on `<name>Change`; if they are equal it does nothing. `update(fn)` goes through `set()`, so the same rule applies. When the **parent** changes the bound expression, Angular writes the new value straight into the underlying signal node, bypassing `set()`, so no change event fires. That asymmetry is what stops a two-way binding from looping: the parent's push is not echoed back, and the child's write reaches the parent exactly once. A practical consequence: mutating an object held by a model and calling `set()` with the same reference emits nothing.
go deeper
Remember that the child's own set or update calls emit the Change event, and parent pushes do not.
Explain the equality check inside set() and why a parent's push bypasses the emitter, which is what keeps a two-way binding from looping.
Spot the silent-event bugs: in-place mutation, duplicate writes and handlers that assume every value change arrives on the output.
Decide which interactions deserve a dedicated output() rather than overloading a model's change event as an activity log.
## The two directions of a model input An Angular **model input**, declared with `model()` from `@angular/core`, carries values in two directions. The **input side** receives values from a parent's property binding (`[open]` or the input half of `[(open)]`). The **output side** is an implicit output named `openChange` that tells the parent about values the child wrote. Knowing exactly which writes produce an event matters, because it explains why two-way bindings do not loop and why some expected events never arrive. ## What the source does In Angular 22.2 a `ModelSignal` is built from an input signal node plus an `OutputEmitterRef`. Its methods behave like this: - **`set(newValue)`** compares `newValue` with the current value using the node's equality function. If they are *not* equal, it stores the value (notifying anything that read the signal) and then emits `newValue` on the change output. If they *are* equal, it returns without storing or emitting. - **`update(fn)`** computes `fn(current)` and passes the result to `set()`, so it inherits exactly the same rule. - **A parent binding** does not call `set()`. When change detection finds that the parent's bound expression has a new value, the framework applies it directly to the signal node. The signal changes and dependants are notified, but the emitter is never touched. ## The rules as a table | Write | Signal value changes? | `<name>Change` emits? | |---|---|---| | Child calls `set(x)` with a new value | yes | yes, with `x` | | Child calls `set(x)` with an equal value | no | no | | Child calls `update(fn)` returning a new value | yes | yes | | Parent's bound expression changes | yes | **no** | | Parent's bound expression stays the same | no | no | ## Why the asymmetry exists Consider `<app-panel [(expanded)]="open" />`. Suppose a parent push also emitted `expandedChange`: 1. The parent sets `open` to `true`. 2. Angular pushes `true` into the child's `expanded`. 3. The child would emit `expandedChange(true)`. 4. The two-way binding would write `true` back into `open`. That round trip is pointless at best, and with values that are never equal (a fresh object every time) it could keep bouncing. Emitting only on the child's own writes means each direction carries a value once: down on a push, up on a child write. ## Consequences you meet in practice - **Mutation is invisible.** If `filters = model({tags: []})` and the child does `this.filters().tags.push('x')` and then `this.filters.set(this.filters())`, the reference is the same, `Object.is` says equal, and nothing emits. Produce a new object: `this.filters.update(f => ({...f, tags: [...f.tags, 'x']}))`. - **Listen-only parents see only user-driven changes.** A parent that binds `(expandedChange)="log($event)"` is told about writes the child made, not about values some other binding pushed in. - **Emitting the same value twice does not happen.** A toggle that calls `set(true)` while already `true` stays silent, so an analytics handler on the change output never counts duplicate clicks from that path. If you need to report every click, that is a separate `output()`, not the model's change event. - **Writes happen synchronously.** `set()` stores the value and emits during the call, so the parent's two-way target is updated inside the same event handler, before the next refresh of the view. ## A short trace ```ts import {Component, model} from '@angular/core'; @Component({ selector: 'app-stepper', template: `<button (click)="step.update(s => s + 1)">{{ step() }}</button>`, }) export class Stepper { step = model(1); } ``` With `<app-stepper [(step)]="current" />` and `current = signal(1)`: 1. A click calls `update`, giving `2`; `set(2)` finds `2 !== 1`, stores it and emits `stepChange(2)`. 2. The two-way binding calls `current.set(2)` in the parent. 3. On the next check the parent's binding sees `2`, which differs from the last value it pushed, and writes `2` into the child's node. The value already equals `2`, so nothing further happens and no event fires. ## Subscribing from code Because `ModelSignal` is also an `OutputRef`, code that holds the instance, such as a test, can call `subscribe()` on it and later `unsubscribe()` the returned subscription. It follows the same rule: only the child's effective writes are delivered. General `output()` subscription mechanics are covered with outputs; the point here is only that the model's emitter is the same kind of object.
- How would you make a model emit when the child changes one field of an object value?Write a new object instead of mutating the old one, for example `this.filters.update(f => ({...f, sort: 'asc'}))`. The new reference fails the `Object.is` check, so `set()` stores it and emits it on the change output. Mutating in place and setting the same reference emits nothing.
- If a parent only binds (valueChange) and never binds [value], what does the child start with?It starts from the initial value given to `model()`, or `undefined` for `model<T>()` with no argument. With `model.required()` the compiler rejects the usage, because the input side is mandatory.
A shared whiteboard with a bell: the child rings the bell only when it writes something new on the board itself. When the parent rewrites the board, the board changes but nobody rings the bell, otherwise the two would ring at each other forever.
saying these in an interview costs you the question
- The change output fires whenever the parent pushes a new value in
- Calling set() with the same value still emits a change event
- Mutating an object inside a model and re-setting it always notifies the parent
- update() skips the change output because it is not set()
- Two-way model bindings loop unless you add a guard yourself