Angular – Inputs, Outputs and Two-Way Binding

June 18, 20264 min readUpdated 8/21/2026

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.