Data goes down through inputs; events come back up through outputs. That is the entire contract between a parent and a child, and Angular now expresses both as functions rather than decorators.
export class Modal {
readonly open = input(false);
readonly size = input<'md' | 'lg'>('md');
readonly scrollable = input(false);
readonly spreadFooter = input(false);
readonly ariaLabel = input<string | null>(null);Used from the parent, those are ordinary property bindings:
<app-cart-drawer [open]="cartOpen()" (closed)="cartOpen.set(false)" />input()
The argument to input() is the default, and the type is usually inferred from it.
When there is no sensible default, or the type is wider than the default suggests, give it
explicitly — input<'md' | 'lg'>('md') above.
An input is a signal. That is the important part. You read it by calling it,
and reading it inside a template or a computed subscribes to it:
readonly isPizza = computed(() => this.product().type === 'PIZZA');
readonly cheapest = computed(() => Math.min(...this.product().sizes.map((s) => s.price)));Those recompute when product changes and at no other time. With the old
@Input() decorator the equivalent was ngOnChanges, a method that fires for
every input with a bag of SimpleChanges to unpick.
Inputs are read-only from inside the component. There is no setter — the parent owns the
value, and a child that could rewrite its own input would make the data flow impossible to follow.
When a child genuinely needs to propose a change, that is what an output, or
model(), is for.
input.required()
readonly product = input.required<Product>();No default, and the type is not Product | undefined. Omit the binding and the
build fails, naming the component — not an undefined at runtime three screens
later.
⚠️ One trap, and it has cost time in this app already: a required input is not
readable from the constructor. It is assigned after construction, so reading one there
throws NG0950 at runtime with nothing failing at build time. Read it in
ngOnInit, or in an effect, or in a computed — anywhere that
runs later.
Input transforms
An input can convert what it receives. The most common case is a boolean attribute, where the value arrives as the empty string:
readonly appAutofocus = input(true, { transform: booleanAttribute });That is what lets appAutofocus on its own mean true while
[appAutofocus]="false" means false. Angular ships booleanAttribute and
numberAttribute; a transform is just a function, so anything with the right shape
works. The declared type stays boolean, so nothing downstream has to think about the
coercion.
output()
readonly closed = output<void>();The parent binds it like a DOM event, and $event is whatever was emitted:
<app-product-card [product]="product" (selected)="selectedProduct.set($event)" />Inside the child, emit() sends it:
<button type="button" class="btn btn-sm btn-primary" (click)="selected.emit(product())">
{{ isPizza() ? 'Build it' : 'Add' }}
</button>output<void>() is the "something happened, there is no payload" case —
closed.emit() with no argument. An output is not a signal; it is a stream of events, and
that distinction is the right one. State is a value you can read at any time; an event is a thing
that happened once.
It replaces @Output() closed = new EventEmitter<void>(). The practical
gains: no EventEmitter import, no way to subscribe to it by accident inside the
component, and automatic cleanup when the component is destroyed.
model(), and two-way binding on your own component
When a value should flow both ways, model() is an input and an output at
once:
readonly value = model(''); // an input `value` AND an output `valueChange`<app-search-box [(value)]="term" />The banana-in-a-box works because Angular looks for an output named after the input with
Change appended, and model() declares both. Unlike a plain input, a
model() is writable from inside the component — this.value.set('…') — and
the write propagates to the parent.
There is no model() in the pizza app. Every child there either takes a value or
reports an event, and none needs to do both, so reaching for it would be ceremony. It is worth
knowing about and worth being slightly suspicious of: two-way binding makes a component convenient
and makes the direction of data harder to see.
Naming
Inputs and outputs are part of a component's public API and read best as such. Inputs are
nouns — product, open, size. Outputs are things that happened,
not handlers: closed, selected. Angular even warns if you prefix an output
with on, because the binding already supplies that at the call site — the parent writes
(closed), and (onClosed) would read as onOnClosed.
What is next
A component that wraps content it does not own: <ng-content>, named slots,
and the templates the modal is built from.