@Input and @Output are the primary mechanisms for parent-child component communication. @Input() allows a parent to pass data down to a child — the child declares what data it needs, the parent provides it. @Output() allows a child to notify its parent about events — the child emits an event, the parent handles it. This unidirectional data flow (data flows down via inputs, events flow up via outputs) is a core Angular design principle that makes components predictable and reusable.
@Input and @Output in Practice
// ── Child component ────────────────────────────────────────────────────────
import { Component, Input, Output, EventEmitter, input, output } from '@angular/core';
import { DatePipe } from '@angular/common';
import { PostSummaryDto } from '@models/post';
@Component({
selector: 'app-post-card',
standalone: true,
imports: [DatePipe],
template: `
<article class="post-card" (click)="onCardClick()">
<h2>{{ post.title }}</h2>
<p class="meta">
By {{ post.authorName }} · {{ post.publishedAt | date:'mediumDate' }}
</p>
<p>{{ post.tags.join(', ') }}</p>
<button (click)="$event.stopPropagation(); onEdit()"
[disabled]="!canEdit">
Edit
</button>
</article>
`,
})
export class PostCardComponent {
// ── Classic @Input decorator ──────────────────────────────────────────────
@Input({ required: true }) post!: PostSummaryDto; // required in Angular 16+
@Input() canEdit = false; // optional with default
// ── Signal-based input (Angular 17+) — alternative syntax ─────────────────
// post = input.required<PostSummaryDto>(); // required signal input
// canEdit = input(false); // optional with default
// ── @Output — event emitter ───────────────────────────────────────────────
@Output() postSelected = new EventEmitter<PostSummaryDto>();
@Output() editPost = new EventEmitter<number>();
// ── Signal-based output (Angular 17+) ─────────────────────────────────────
// postSelected = output<PostSummaryDto>();
// editPost = output<number>();
onCardClick(): void {
this.postSelected.emit(this.post); // emit the whole post to parent
}
onEdit(): void {
this.editPost.emit(this.post.id); // emit just the ID to parent
}
}
// ── Parent component ──────────────────────────────────────────────────────
@Component({
selector: 'app-post-list',
standalone: true,
imports: [PostCardComponent],
template: `
@for (post of posts; track post.id) {
<!-- Pass data DOWN via [input] ────────────────────────────────────── -->
<app-post-card
[post]="post"
[canEdit]="currentUserId === post.authorId"
(postSelected)="onPostSelected($event)"
(editPost)="onEditPost($event)"
/>
}
`,
})
export class PostListComponent {
posts = signal<PostSummaryDto[]>([]);
currentUserId = 'user-123';
// $event contains what the child emitted
onPostSelected(post: PostSummaryDto): void {
console.log('Post selected:', post.title);
}
onEditPost(postId: number): void {
// Navigate to edit page
}
}
@Input({ required: true }) to mark inputs as required — if a parent forgets to provide the input, Angular throws a compile-time error rather than silently leaving the value undefined. This catches a common category of bugs where components are used without all their required data. Mark all inputs that a component cannot function without as required. Provide sensible defaults for optional inputs rather than leaving them potentially undefined.post = input.required<PostSummaryDto>()) in Angular 17+ for inputs that benefit from signal integration. Signal inputs are automatically tracked in computed signals and effects — changing a signal input triggers reactive updates without ngOnChanges. Signal outputs (postSelected = output<PostSummaryDto>()) are lighter-weight than EventEmitter. The classic decorator-based syntax still works and is widely used — both approaches are valid in Angular 18.get displayTitle() { return this.post.title.toUpperCase(); }. If the child needs to tell the parent about a change, emit an @Output event and let the parent decide whether and how to update the data.Common Mistakes
Mistake 1 — Modifying @Input properties in the child (breaks unidirectional flow)
❌ Wrong — this.post.title = 'modified' inside child; mutates parent’s object; hard to trace bugs.
✅ Correct — emit an output event; let the parent handle state updates.
Mistake 2 — Emitting an event in a click handler without stopping propagation (double trigger)
❌ Wrong — inner button (click) bubbles to parent article (click); both handlers fire.
✅ Correct — use $event.stopPropagation() on the inner element to prevent bubbling.