Komunikacja między komponentami w Angularze

1. Wprowadzenie – rodzina komponentów

Wyobraź sobie komponenty jako rodzinę:

  • Rodzic (komponenta główna) – ma dzieci
  • Dzieci (komponenty potomne) – należą do rodzica
  • Rodzeństwo (komponenty na tym samym poziomie) – dzieci tego samego rodzica

Komunikacja w aplikacji filmowej:

  • Rodzic: Lista filmów
  • Dziecko: Karta pojedynczego filmu
  • Rodzeństwo: Różne karty filmów

2. Typy komunikacji

🔽 RODZIC → DZIECKO (@Input)

  • Rodzic przekazuje dane do dziecka
  • Np. Lista filmów przekazuje dane o filmie do karty filmu

🔼 DZIECKO → RODZIC (@Output + EventEmitter)

  • Dziecko wysyła informację do rodzica
  • Np. Karta filmu informuje listę o kliknięciu „Usuń”

↔️ RODZEŃSTWO ↔ RODZEŃSTWO (Service)

  • Komponenty na tym samym poziomie wymieniają dane
  • Np. Karta filmu informuje inne karty o zmianie

3. Komunikacja RODZIC → DZIECKO (@Input)

Jak to działa?

  1. Rodzic ma dane
  2. Dziecko deklaruje @Input
  3. Rodzic przekazuje dane przez HTML

Przykład: Lista filmów → Karta filmu

Krok 1: Dziecko przyjmuje dane (@Input)

// film-card.component.ts (DZIECKO)
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-film-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ film.title }}</h3>
      <p>Rok: {{ film.year }}</p>
      <p>Ocena: {{ film.rating }}/10</p>
    </div>
  `,
  styles: [`
    .card {
      border: 1px solid #ddd;
      padding: 15px;
      margin: 10px;
      border-radius: 8px;
    }
  `]
})
export class FilmCardComponent {
  @Input() film!: any; // ! oznacza "na pewno otrzymam te dane"
}

Krok 2: Rodzic przekazuje dane

// film-list.component.ts (RODZIC)
import { Component } from '@angular/core';
import { FilmCardComponent } from './film-card.component';

@Component({
  selector: 'app-film-list',
  standalone: true,
  imports: [FilmCardComponent],
  template: `
    <h2>Lista filmów</h2>
    @for (film of films; track film.id) {
      <app-film-card [film]="film"></app-film-card>
    }
  `
})
export class FilmListComponent {
  films = [
    { id: 1, title: 'Avengers', year: 2019, rating: 8.4 },
    { id: 2, title: 'Batman', year: 2022, rating: 7.8 },
    { id: 3, title: 'Spider-Man', year: 2021, rating: 8.2 }
  ];
}

Wyjaśnienie:

  • @Input() film!: any – dziecko mówi: „Oczekuję danych o nazwie 'film'”
  • [film]="film" – rodzic mówi: „Przekazuję ci dane o filmie”
  • ! oznacza „na pewno otrzymam te dane od rodzica”

4. Komunikacja DZIECKO → RODZIC (@Output + EventEmitter)

Jak to działa?

  1. Dziecko tworzy wydarzenie (@Output + EventEmitter)
  2. Dziecko wysyła wydarzenie (.emit())
  3. Rodzic słucha wydarzenia w HTML

Przykład: Karta filmu → Lista filmów

Krok 1: Dziecko tworzy wydarzenie

// film-card.component.ts (DZIECKO)
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-film-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ film.title }}</h3>
      <p>Rok: {{ film.year }}</p>
      <p>Ocena: {{ film.rating }}/10</p>
      
      <button (click)="likeFilm()" class="btn-like">
        ❤️ Polub
      </button>
      <button (click)="deleteFilm()" class="btn-delete">
        🗑️ Usuń
      </button>
    </div>
  `,
  styles: [`
    .card { border: 1px solid #ddd; padding: 15px; margin: 10px; }
    button { margin: 5px; padding: 5px 10px; cursor: pointer; }
    .btn-like { background: #28a745; color: white; }
    .btn-delete { background: #dc3545; color: white; }
  `]
})
export class FilmCardComponent {
  @Input() film!: any;
  
  // Tworzę wydarzenia, które mogę wysłać do rodzica
  @Output() filmLiked = new EventEmitter<any>();
  @Output() filmDeleted = new EventEmitter<number>();
  
  likeFilm() {
    // Wysyłam cały obiekt filmu do rodzica
    this.filmLiked.emit(this.film);
  }
  
  deleteFilm() {
    // Wysyłam tylko ID filmu do rodzica
    this.filmDeleted.emit(this.film.id);
  }
}

Krok 2: Rodzic słucha wydarzeń

// film-list.component.ts (RODZIC)
import { Component } from '@angular/core';
import { FilmCardComponent } from './film-card.component';

@Component({
  selector: 'app-film-list',
  standalone: true,
  imports: [FilmCardComponent],
  template: `
    <h2>Lista filmów</h2>
    <p>Polubionych filmów: {{ likedFilms.length }}</p>
    
    @for (film of films; track film.id) {
      <app-film-card 
        [film]="film"
        (filmLiked)="onFilmLiked($event)"
        (filmDeleted)="onFilmDeleted($event)">
      </app-film-card>
    }
    
    <!-- Lista polubionych filmów -->
    @if (likedFilms.length > 0) {
      <div class="liked-films">
        <h3>Polubione filmy:</h3>
        @for (film of likedFilms; track film.id) {
          <p>{{ film.title }}</p>
        }
      </div>
    }
  `,
  styles: [`
    .liked-films {
      background: #f8f9fa;
      padding: 15px;
      border-radius: 8px;
      margin-top: 20px;
    }
  `]
})
export class FilmListComponent {
  films = [
    { id: 1, title: 'Avengers', year: 2019, rating: 8.4 },
    { id: 2, title: 'Batman', year: 2022, rating: 7.8 },
    { id: 3, title: 'Spider-Man', year: 2021, rating: 8.2 }
  ];
  
  likedFilms: any[] = [];
  
  // Gdy dziecko wysyła wydarzenie "filmLiked"
  onFilmLiked(film: any) {
    console.log('Film polubiony:', film.title);
    
    // Dodaj do listy polubionych (jeśli jeszcze nie ma)
    if (!this.likedFilms.find(f => f.id === film.id)) {
      this.likedFilms.push(film);
    }
  }
  
  // Gdy dziecko wysyła wydarzenie "filmDeleted"
  onFilmDeleted(filmId: number) {
    console.log('Usuwam film o ID:', filmId);
    
    // Usuń z listy filmów
    this.films = this.films.filter(f => f.id !== filmId);
    
    // Usuń z listy polubionych
    this.likedFilms = this.likedFilms.filter(f => f.id !== filmId);
  }
}

Wyjaśnienie:

  • @Output() filmLiked = new EventEmitter<any>() – tworzę wydarzenie
  • this.filmLiked.emit(this.film) – wysyłam dane do rodzica
  • (filmLiked)="onFilmLiked($event)" – rodzic słucha i reaguje
  • $event – to dane wysłane przez dziecko

Porównanie sposobów komunikacji

Typ komunikacjiKiedy używaćJak to działaPrzykład
@InputRodzic → DzieckoPrzekazanie danychLista przekazuje film do karty
@OutputDziecko → RodzicWysłanie wydarzeńKarta informuje o kliknięciu
ServiceRodzeństwo ↔ RodzeństwoWspólny „posłaniec”Karty komunikują się między sobą

7. Najważniejsze zasady

✅ Co robić:

  1. @Input dla danych – gdy rodzic ma dane dla dziecka
  2. @Output dla wydarzeń – gdy dziecko chce coś przekazać rodzicom
  3. Service dla rodzeństwa – gdy komponenty na tym samym poziomie muszą się komunikować
  4. Unsubscribe w ngOnDestroy – zawsze przy Service

❌ Czego unikać:

  1. Przekazywania danych „w górę” przez @Input – to nie działa
  2. Zapominania o unsubscribe – wyciek pamięci
  3. Używania Service do prostej komunikacji rodzic-dziecko

8. Praktyczne wskazówki

Debugowanie komunikacji:

// Dodaj console.log aby zobaczyć co się dzieje
@Input() set film(value: any) {
  console.log('Otrzymuję nowe dane:', value);
  this._film = value;
}

onFilmLiked(film: any) {
  console.log('Otrzymałem wydarzenie:', film);
  // reszta logiki...
}

Typowanie dla bezpieczeństwa:

// Zamiast 'any' używaj konkretnych typów
interface Film {
  id: number;
  title: string;
  year: number;
  rating: number;
}

@Input() film!: Film;
@Output() filmLiked = new EventEmitter<Film>();

9. Ćwiczenia do wykonania

  1. Sklep filmowy: Lista filmów → Koszyk (dodawanie filmów)
  2. System ocen: Karta filmu → Lista (aktualizacja średniej oceny)