input(), output(), and model() Signal Primitives
- Last updated
- Prerequisites:
- Angular component basics (@Input/@Output)
- Understanding of Angular signals
- angular
- signals
- input
- output
- model
- component-api
Read at your depth
The practical view
Angular components traditionally declare inputs with @Input() and outputs with @Output() + EventEmitter. The signal-based API replaces them: count = input(0); gives a signal input (read count() in templates/code), and changed = output<number>(); emits like an EventEmitter (this.changed.emit(5)). For two-way binding, value = model(0); creates a model input that also emits updates when you set it — [(value)] works with zero boilerplate. Signal inputs are read-only from the child's perspective (you cannot assign to count(); you can only read it). This API is the modern, type-safe replacement for decorator-based bindings.
The same idea in other frameworks
Props are inputs; model() resembles a controlled value + onChange pair bundled together.input()/output()/model() — the current topic.defineProps + defineEmits in <script setup>; defineModel() is Vue's analogue of Angular model().Svelte 5 $props and $bindable mirror the same one-way input + two-way binding split.Legacy vs modern
Decorator @Input/@Output vs signal input()/output()/model()
Decorators are strings (class-field based) with EventEmitter boilerplate; signal primitives are typed, reactive, and model() removes the emit boilerplate for two-way bindings.
@Input() count = 0; // ❌ OLD: string-keyed, non-reactive field
@Output() changed = new EventEmitter<number>();
@Output() countChange = new EventEmitter<number>(); // ❌ OLD: manual model plumbingcount = input(0); // ✅ NEW: typed, reactive, read-only input
changed = output<number>();
countModel = model(0); // ✅ NEW: two-way binding in one primitiveInterview gotchas
Context
This tests whether you understand the reactivity model shift — that inputs as signals create granular dependencies rather than whole-component input-change triggers.
The mechanical answer
An @Input() is a plain field; when it changes, the framework marks the component dirty and re-checks it (and its template) as a unit. A signal input is a reactive node: template bindings that read count() register as consumers, so when the parent writes a new value, only those specific bindings re-evaluate — aligned with zoneless, granular change detection. Signal inputs are also read-only from the child: you cannot assign to an input() signal (the compiler enforces this), which prevents the anti-pattern of mutating inputs locally. The trade-off: you cannot use signal inputs with template reference variables the same way, and the value must be read with () or used inside reactive contexts; also, non-signal consumption (e.g. in ngOnInit) requires reading once and storing, losing reactivity.
Trap
The naive answer is 'signal inputs are just typed @Input'. They change the invalidation granularity and add read-only semantics. Another trap: trying to assign to an input signal inside the child or expecting changes to reflect when you read the value once and store it in a plain variable — you must read it in a reactive context (template, computed, effect). Candidates who mention consumer registration and read-only enforcement show real understanding.
Context
This probes the mechanical details of model() — the implicit change output and how [(x)] sugar compiles — a favorite area for interviewers probing signal familiarity.
The mechanical answer
model() creates a two-way binding primitive that is both an input signal and an output. In the parent template, [(value)]="x" desugars to [value]="x" plus (valueChange)="x = $event". The model() call registers the input side (value) and automatically creates the output side (valueChange) — so when the child calls this.value.set(newVal) or this.value.update(fn), the component emits the valueChange event synchronously with the new value, and the parent's expression updates x. This removes the manual [x] + (xChange) + EventEmitter boilerplate. The compiler knows the model's name to generate the Change output name. Models also support required models (model.required()) and work with signals in both directions.
Trap
The naive answer is 'model() is just an input that can be written'. The write path emits valueChange, and if the parent is not using [(value)] syntax, the child's set has no effect on the parent. Another trap: assuming model() and input()+output() are interchangeable — model() carries the two-way contract and emits the Change event on every set; a plain input is read-only and a separate output requires manual emit wiring. Candidates who can state the valueChange event name and the [(x)] desugaring demonstrate precise knowledge.