Angular · Architecture

Component Communication

Components rarely work alone. A list tells a row what to show; the row tells the list it was deleted; two distant widgets share a cart. Angular has a clear tool for each direction.

Advanced 6 min Lesson 28 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
0/38 in Angular
On this page · 6 min · Advanced

Learning objectives#

  • Pass data down with input()
  • Send events up with output()
  • Call a child directly with viewChild
  • Share data between siblings with a service and project content with ng-content

Explanation#

Parent → child: input(). The parent binds [product]="p"; the child reads product().

Child → parent: output(). The child declares deleted = output<number>() and calls this.deleted.emit(id); the parent listens with (deleted)="remove($event)".

Both ways: model() for an editable value, bound with [(value)].

Parent calling a child method: rating = viewChild(RatingComponent) and then this.rating()?.reset(). Use sparingly — inputs and outputs keep components decoupled.

Siblings or distant components: share a service with signals (a cart, the current user).

Content projection: a Card component with <ng-content /> displays whatever the parent puts between its tags; named slots use <ng-content select="[card-footer]" />.

Syntax#

TypeScript Illustrative
// child
product = input.required<Product>();
deleted = output<number>();
remove() { this.deleted.emit(this.product().id); }


// parent template
<app-product-row [product]="p" (deleted)="remove($event)" />

Code example: A list, rows, a card with projected content, and viewChild#

TypeScripttodo-board.ts Illustrative
import { Component, input, output, signal, viewChild } from '@angular/core';


type Todo = { id: number; title: string; done: boolean };


@Component({
  selector: 'app-card',
  template: `
    <section class="card">
      <header><ng-content select="[card-title]" /></header>
      <ng-content />
      <footer><ng-content select="[card-footer]" /></footer>
    </section>
  `,
  styles: `.card { border: 1px solid #ddd; border-radius: 8px; padding: 8px; }`,
})
export class Card {}


@Component({
  selector: 'app-todo-row',
  template: `
    <label>
      <input type="checkbox" [checked]="todo().done" (change)="toggled.emit(todo().id)" />
      {{ todo().title }}
    </label>
    <button (click)="deleted.emit(todo().id)">✕</button>
  `,
})
export class TodoRow {
  todo = input.required<Todo>();
  toggled = output<number>();
  deleted = output<number>();
}


@Component({
  selector: 'app-new-todo',
  template: `<input #box (keyup.enter)="add(box.value); box.value = ''" placeholder="New todo" />`,
})
export class NewTodo {
  created = output<string>();
  count = 0;
  add(title: string) {
    if (title.trim()) { this.created.emit(title.trim()); this.count++; }
  }
  reset() { this.count = 0; }
}


@Component({
  selector: 'app-todo-board',
  imports: [Card, TodoRow, NewTodo],
  template: `
    <app-card>
      <h3 card-title>My todos ({{ remaining() }} left)</h3>
      @for (t of todos(); track t.id) {
        <app-todo-row [todo]="t" (toggled)="toggle($event)" (deleted)="remove($event)" />
      }
      <app-new-todo (created)="add($event)" />
      <small card-footer>Added this session: {{ newTodo()?.count ?? 0 }} <button (click)="resetCounter()">reset</button></small>
    </app-card>
  `,
})
export class TodoBoard {
  todos = signal<Todo[]>([{ id: 1, title: 'Learn inputs', done: true }, { id: 2, title: 'Learn outputs', done: false }]);
  newTodo = viewChild(NewTodo);
  private nextId = 3;


  remaining() { return this.todos().filter(t => !t.done).length; }
  add(title: string) { this.todos.update(l => [...l, { id: this.nextId++, title, done: false }]); }
  toggle(id: number) { this.todos.update(l => l.map(t => (t.id === id ? { ...t, done: !t.done } : t))); }
  remove(id: number) { this.todos.update(l => l.filter(t => t.id !== id)); }
  resetCounter() { this.newTodo()?.reset(); }
}
Expected output
A bordered card titled “My todos (1 left)” lists two todos. Ticking a box or clicking ✕ emits an event the board handles, and the title updates. Typing a todo and pressing Enter adds it and increases “Added this session”. The reset button calls NewTodo.reset() through viewChild.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. TodoRow receives a todo via input and reports toggles and deletions via outputs — it never changes the list itself.
  2. TodoBoard owns the list and reacts to $event (the emitted id).
  3. NewTodo emits the new title; the board decides how to store it.
  4. viewChild(NewTodo) gives the board direct access to call reset().
  5. Card projects content: the title, body and footer come from the parent.

Real-world use case#

A data table component takes rows and columns as inputs, emits (sortChange) and (rowSelected) outputs, and projects custom cell templates — reused across dozens of screens.

Common mistakes#

Children modifying parent data

A row that edits the shared array directly makes data flow hard to follow. Emit an event; let the owner change the data.

Overusing viewChild

Calling many child methods creates tight coupling. Prefer inputs and outputs.

Deep input/output chains

Passing the same data through five levels is painful. Use a shared service.

Best practices#

  • Data down with inputs, events up with outputs
  • One owner per piece of state
  • Services for siblings and distant components
  • ng-content for layout components

Remember this#

input() down · output() up · model() both ways.

Siblings → shared service.

<ng-content /> = parent decides the content.

Practice exercise#

Your task: Create a Rating component with a stars input and a rated output that emits 1–5 when a star is clicked, and a parent that shows "You rated 4 stars".

Need a hint?
In the child: @for over [1,2,3,4,5] with (click)="rated.emit(n)".
Show solution
TypeScript Illustrative
import { Component, input, output, signal } from '@angular/core';


@Component({
  selector: 'app-rating',
  template: `@for (n of [1, 2, 3, 4, 5]; track n) {
    <button (click)="rated.emit(n)" [attr.aria-label]="n + ' stars'">{{ n <= stars() ? '★' : '☆' }}</button>
  }`,
})
export class Rating {
  stars = input(0);
  rated = output<number>();
}


@Component({
  selector: 'app-review',
  imports: [Rating],
  template: `<app-rating [stars]="stars()" (rated)="stars.set($event)" /> <p>You rated {{ stars() }} stars</p>`,
})
export class Review {
  stars = signal(0);
}

Mini quiz#

Component Communication quizMultiple choice Question 1 of 3

How does a child notify its parent?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.