Component Inputs and Outputs
An Angular app is a tree of components, and the tree needs plumbing: data flowing down, events
flowing up. Modern Angular expresses both as signal-based functions - input(), output(),
model() - which slot straight into the reactivity you learned last phase: an input is a
signal, so computeds and templates track it like any other. Same one-way philosophy as the rest of
the family; Angular's version comes with the types checked.
input(): data down, as a signal
// product-card.ts
;
<!-- parent template -->
What just happened: each input() declares a prop - required ones the compiler refuses to
let a parent omit, optional ones with defaults. At the call site, the phase 2 rule applies:
brackets bind expressions, bare attributes pass strings ([price]="4900" is the number;
price="4900" is a string, and with input.required<number>() the build fails - the
TypeScript payoff in action).
Because inputs are signals, displayPrice is just a computed over them: parent passes a new
price, the computed and the template update through the standard graph. No lifecycle hook to
intercept changes (ngOnChanges, the legacy way) - dependency tracking replaces it.
output(): events up
;
<!-- parent template -->
What just happened: output<number>() declares a typed event; the child fires it with
.emit(payload); the parent listens with the same (round brackets) used for DOM events, and
$event is the payload. The child announces intent, the parent owns the meaning - data down,
events up, with both directions in the class's first lines:
💡 Key point: a component's input()/output() declarations are its documentation - typed,
compiler-checked, and complete. This is Angular's version of the interface-first habit, and it's
why sprawling Angular codebases stay navigable: the contract is always at the top of the class.
model(): two-way, composed
For genuinely co-owned values - the search box, the rating widget - model() declares an input
and its matching output as one:
// star-rating.ts
;
<!-- parent -->
What just happened: model(0) is a writable input - the child may set it, and each set
emits a valueChange event under the hood. The parent's banana-in-a-box [(value)] wires both
directions. It's the same de-sugaring as [(ngModel)] in phase 2, now for your own components -
and the same consent principle as Svelte's $bindable: two-way exists only where the child
explicitly declares a model, never by ambush.
Reading the legacy dialect
Most existing code declares its interface with decorators; the mapping is one-to-one:
| Legacy | Modern | Notes |
|---|---|---|
@Input() name: string; |
name = input<string>() |
legacy inputs are plain fields, not signals - read this.name, no parens |
@Input({ required: true }) |
input.required<string>() |
|
@Output() add = new EventEmitter<number>(); |
add = output<number>() |
both fire with .emit(...) |
ngOnChanges(changes) {...} |
a computed or effect over the input signal |
the hook watched inputs by hand |
The practical wrinkle: in legacy components, inputs are ordinary properties - templates say
{{ name }} not {{ name() }}. When you're editing a component, check which dialect it speaks
before adding parentheses (or forgetting them); phase 7's cheat-card includes the two mismatch
errors this produces.
Recap
input()declares props as signals -requiredenforced at build time, defaults for the rest; computeds over inputs replacengOnChanges.output<T>()+.emit(payload)sends typed events up; parents listen with(eventName)and read$event.model()is the declared two-way: writable input + implicitxChangeoutput, bound with[(x)].- The input/output block at the top of a class is the component's compiler-checked contract.
- Legacy dialect:
@Input/@Outputdecorators, non-signal fields,ngOnChanges- translate on sight, don't mix within a component.
[
{
"q": "A parent writes <app-badge [count]=\"5\" /> but the build fails: required input 'label' has no value. What's Angular enforcing?",
"choices": [
"All inputs must always be provided",
"The child declared label = input.required(), and required inputs are checked at compile time",
"Inputs must be strings unless bracketed",
"The selector app-badge is not imported"
],
"answer": 1,
"why": [
"Optional inputs with defaults omit freely - only required ones are enforced.",
null,
"That's the string-vs-expression rule; this error is about absence, not type.",
"A missing import is the unknown-element error (phase 7), not a missing-input one."
],
"explain": "input.required() moves 'this prop is mandatory' from documentation into the compiler. The parent that forgets it gets a build error, not an undefined at runtime."
},
{
"q": "Legacy component: @Input() title: string. You write {{ title() }} in its template and get a runtime error. Why?",
"choices": [
"Legacy inputs need the async pipe",
"Decorator inputs are plain properties, not signals - there's nothing to call; it's {{ title }}",
"The parentheses conflict with the event-binding syntax",
"title must be initialized before use"
],
"answer": 1,
"why": [
"The async pipe unwraps observables (phase 6) - unrelated to plain fields.",
null,
"() in interpolation is a call, not an event binding - the call itself is the problem.",
"Initialization affects the value, not the calling-a-string crash."
],
"explain": "The two dialects differ exactly here: signal inputs read with parens, decorator inputs without. Check which dialect a component speaks before editing its template."
},
{
"q": "When does model() beat a plain input() plus output() pair?",
"choices": [
"Whenever a component has both inputs and outputs",
"When parent and child genuinely co-own one value - form-like widgets where [(x)] binding is the natural interface",
"Always - it's the modern replacement",
"When the value is an object rather than a primitive"
],
"answer": 1,
"why": [
"Most components have both - a product card's price in and addToCart out are two different values, not one shared one.",
null,
"One-way input/output remains the default and the majority - model() is the declared exception.",
"Shape of the value is irrelevant; ownership is the criterion."
],
"explain": "model() is for the rating-widget/search-box case: one value both sides read and write. Everything else stays one-way - data down, events up, traceable."
}
]
← Phase 3: Signals · Guide overview · Phase 5: Services and Dependency Injection →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. A parent writes <app-badge [count]="5" /> but the build fails: required input 'label' has no value. What's Angular enforcing?
2. Legacy component: @Input() title: string. You write {{ title() }} in its template and get a runtime error. Why?
3. When does model() beat a plain input() plus output() pair?