A component talks to its parent through inputs (data coming in) and outputs (events going out). The classic rule still holds: data down, events up. What has changed is the API: input() and output() are functions, and inputs are signals.
A complete example
import { Component, booleanAttribute, computed, input, numberAttribute, output } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
@for (star of stars(); track star) {
<button [class.on]="star <= value()" [disabled]="readonly()" (click)="rate.emit(star)">★</button>
}
`,
})
export class StarRating {
readonly value = input.required<number>(); // required
readonly max = input(5, { transform: numberAttribute }); // max="10" → 10
readonly readonly = input(false, { transform: booleanAttribute }); // <app-star-rating readonly />
readonly rate = output<number>();
// inputs are signals → derive directly with computed, no ngOnChanges
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}- Give a rating — the parent receives the event (rate).
Options: required, transform, alias
| Form | Type | When |
|---|---|---|
input(5) | InputSignal<number> | Has a reasonable default value. |
input<string>() | InputSignal<string | undefined> | Truly optional. |
input.required<User>() | InputSignal<User> | The component makes no sense without it. |
transform: booleanAttribute | accepts '', 'true' | Flags: <app-x disabled />. |
transform: numberAttribute | accepts '10' | Numbers written as static attributes. |
// Alias (the name in the template differs from the one in the class) — rarely used
readonly label = input('', { alias: 'ariaLabel' });
// Custom transform: normalise the value on input
readonly tags = input([], {
transform: (value: string | string[]) => (Array.isArray(value) ? value : value.split(',')),
});
// Optional input without a default value → the type includes undefined
readonly hint = input<string>(); // InputSignal<string | undefined>
// Output with an alias
readonly closed = output<void>({ alias: 'close' });
this.closed.emit();Before and now
// ✓ The modern API: signals + functions
export class StarRating {
readonly value = input.required<number>();
readonly max = input(5, { transform: numberAttribute });
readonly rate = output<number>();
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}// ✗ The old API, based on decorators
export class StarRating implements OnChanges {
@Input({ required: true }) value!: number;
@Input({ transform: numberAttribute }) max = 5;
@Output() rate = new EventEmitter<number>();
stars: number[] = [];
ngOnChanges(changes: SimpleChanges) {
if (changes['max']) {
this.stars = Array.from({ length: this.max }, (_, i) => i + 1);
}
}
}Automatic migration: ng generate @angular/core:signal-input-migration and ng generate @angular/core:outputs.
Inputs are read-only
export class Pager {
readonly page = input(1);
next() {
// ✗ inputs are read-only inside the component
// this.page.set(this.page() + 1);
// ✓ options:
// - emit an event and let the parent decide: this.pageChange.emit(this.page() + 1)
// - use model() if it is a two-way value → see the model() page
// - use linkedSignal(() => this.page()) for a local copy
}
}Subscribing to outputs programmatically
// You can subscribe programmatically to an output (e.g. on a dynamically created component)
const ref = viewContainer.createComponent(StarRating);
const sub = ref.instance.rate.subscribe((value) => console.log(value));
// sub.unsubscribe(); — happens automatically when the component is destroyed
// Or more simply, with bindings at creation (see “Dynamic components”):
viewContainer.createComponent(StarRating, {
bindings: [inputBinding('value', this.rating), outputBinding('rate', (v) => this.save(v))],
});- The compiler throws an error if you bind the same input/output twice on one element.
data-*attributes no longer bind inputs or outputs (e.g.data-valueno longer reaches thevalueinput).- v22.2: the
strictUnclaimedEventNamesoption catches misspelled outputs ((rated)instead of(rate)).
Three levels down: prop drilling
Inputs and outputs connect a parent with its direct child. When the data lives at the top and is needed three levels down, every component in between has to declare an input to pass it on, and an output to send the events back up — even though none of them cares about the data.
signal: countinput: countinput: countoutput: addedoutput: addedoutput: addedoutput: added- Inputs declared
- 2
- Outputs declared
- 4
- Components that know about the cart
- 7
- Press Add to cart.
With inputs and outputs, ProductCard, ProductGrid and ShopPage exist only to relay the event, and each one is another place to break when the event changes shape. With a store, only the two components that actually use the cart know about it. Keep inputs and outputs for the direct relationship — they make a component reusable and its contract visible — and reach for a service when the data has to cross levels or reach a sibling.
// every level in between declares an output only to pass the click on
@Component({
selector: 'app-product-grid',
template: `<app-product-card (added)="added.emit()" />`,
})
export class ProductGrid {
readonly added = output();
}
// ProductCard, ShopPage: the same three lines, once more each
// …and the count travels down the other branch through more inputs
@Component({
template: `
<app-header [count]="count()" />
<app-shop-page (added)="count.update((n) => n + 1)" />
`,
})
export class Shop {
protected readonly count = signal(0);
}When YES, when NO
- Direct parent ↔ child communication.
- Dumb components / reusable UI (buttons, cards, tables).
- Events with meaning:
(save),(delete),(select).
- A value that must be synchronised in both directions →
model(). - Data passed through 3+ levels (“prop drilling”) → an injected service/store.
- Communication between sibling components without a useful common parent → a service.
- Global configuration (theme, language) →
InjectionToken/ service.
Common pitfalls
- Reading
this.value()in the constructor: the inputs are not set yet (required inputs throw an error). Use them incomputed,effector the template. - Objects mutated by the child: if you receive an object through an input and modify it, you silently change the parent’s state. Emit an event instead.
- Outputs named like DOM events (
click,change) cause confusion — the native event bubbles and triggers the handler twice. Choose specific names.
Cheat sheet
input<T>() // InputSignal<T | undefined>
input(default) // InputSignal<T>
input.required<T>() // required, compile error if missing
input(x, { transform, alias }) // booleanAttribute, numberAttribute, your own function
output<T>() // OutputEmitterRef<T> → .emit(value)
output<T>({ alias })
// In the parent's template
<app-x [value]="sig()" flag (changed)="onChanged($event)" />