@Input and @Output — Parent-Child Component Communication

@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
  }
}
Note: Angular 16+ introduced @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.
Tip: Use the signal-based input API (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.
Warning: Never modify an @Input property directly inside the child component. Inputs flow from parent to child — modifying the input breaks the parent’s source of truth and creates hard-to-debug inconsistencies. If the child needs to derive a modified value from an input, use a computed property or getter: 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.

🧠 Test Yourself

A PostCardComponent receives a PostSummaryDto @Input. The parent updates the array with a new post object. With the default change detection, does the card re-render?