Komunikacja między komponentami

Komunikacja między komponentami to kluczowy aspekt budowania aplikacji w Angularze, umożliwiający wymianę danych i wyzwalanie zachowań między różnymi częściami aplikacji. Istnieje kilka głównych sposobów na realizację tej komunikacji, w zależności od relacji między komponentami: komunikacja z rodzica na dziecko, z dziecka na rodzica oraz między komponentami rodzeństwa.

1. Komunikacja z rodzica na dziecko

Najprostszym sposobem przekazywania danych z komponentu rodzica do dziecka jest użycie @Input(). To pozwala na jednokierunkowe przekazywanie danych.

Dekorator @Input() w Angularze jest używany do przekazywania danych z komponentu rodzica do komponentu dziecka. To fundamentalny mechanizm komunikacji w Angularze, który umożliwia jednokierunkowe wiązanie danych (z góry na dół) między komponentami. Dzięki temu można efektywnie oddzielić komponenty, zapewniając im niezbędne dane do wyświetlenia lub dalszej logiki, bez konieczności udostępniania całego kontekstu aplikacji.

@Input() pozwala komponentowi dziecku deklarować właściwości, które mogą przyjąć wartości z zewnątrz, czyli od komponentu rodzica. Rodzic może przekazać wartość do dziecka, dodając atrybut do selektora dziecka w swoim szablonie, który odpowiada nazwie właściwości oznaczonej jako @Input().

Przykład

Rodzic komponent:

// parent.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [CommonModule],
  template: `
    <app-child [message]="parentMessage"></app-child>
  `,
})
export class ParentComponent {
  parentMessage = 'Wiadomość od rodzica';
}

Dziecko komponent:

// child.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [CommonModule],
  template: `<p>Odebrane: {{ message }}</p>`,
})
export class ChildComponent {
  @Input() message!: string;
}

Użycie znaku wykrzyknika (!) przy @Input()

Znak wykrzyknika (!), używany przy właściwościach w TypeScript (tzw. „definite assignment assertion”), informuje kompilator, że właściwość będzie na pewno zainicjowana, ale nie przez konstruktor klasy, a przez Angulara w trakcie procesu ustawiania danych wejściowych (@Input()). Dzięki temu unikamy błędów kompilacji związanych z surowym sprawdzaniem typów i inicjalizacji właściwości, które mogą wystąpić, gdy strictPropertyInitialization jest włączone.

W powyższym przykładzie, deklaracja message!: string; informuje kompilator, że właściwość message zostanie zainicjowana przez Angular przy ustawianiu wartości wejściowej, a nie przez konstruktor komponentu. Dzięki temu nie musimy dawać wartości domyślnej lub fałszywego inicjalizatora, co pozwala zachować czystość kodu i zgodność z rzeczywistymi intencjami logiki aplikacji.

Komunikacja z dziecka na rodzica

Aby przekazać dane z komponentu dziecka do rodzica, można użyć @Output() i EventEmitter. To umożliwia dziecku wysyłanie zdarzeń, które rodzic może nasłuchiwać.

Dekorator @Output() jest używany w klasach komponentów do deklaracji właściwości jako „wyjścia” komponentu. Właściwości te są zazwyczaj instancjami EventEmitter i służą do emitowania danych z komponentu dziecka do komponentu rodzica. Są one podłączane do zdarzeń w szablonie rodzica, umożliwiając rodzicom reagowanie na zmiany lub akcje wykonane w komponencie dziecka.

EventEmitter jest klasą z Angulara, która pozwala komponentom emitować zdarzenia niestandardowe. Można się na nie subskrybować, co jest analogiczne do słuchania na zdarzenia DOM jak click czy hover, ale w kontekście danych komponentów Angulara. EventEmitter jest często używany z @Output() do tworzenia niestandardowych zdarzeń, które mogą być przechwytywane przez komponenty rodzica.

Przykład

Dziecko komponent:

// child.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [CommonModule],
  template: `<button (click)="sendEvent()">Wyślij do rodzica</button>`,
})
export class ChildComponent {
  @Output() notify: EventEmitter<string> = new EventEmitter<string>();

  sendEvent() {
    this.notify.emit('Wiadomość wysłana do rodzica');
  }
}

Rodzic komponent:

// parent.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [CommonModule],
  template: `
    <app-child (notify)="handleEvent($event)"></app-child>
    <p>Otrzymano: {{ messageFromChild }}</p>
  `,
})
export class ParentComponent {
  messageFromChild: string;

  handleEvent(event: string) {
    this.messageFromChild = event;
  }
}

Komunikacja między komponentami rodzeństwa

Do komunikacji między komponentami, które nie mają bezpośredniej relacji rodzic-dziecko, można wykorzystać usługę (Service). Usługi w Angularze pozwalają na współdzielenie danych i logiki pomiędzy dowolnymi komponentami.

Przykład

Shared Service:

// shared.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SharedService {
  private messageSource = new Subject<string>();
  message$ = this.messageSource.asObservable();

  sendMessage(message: string) {
    this.messageSource.next(message);
  }
}

Komponent wysyłający:

// sender.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedService } from './shared.service';

@Component({
  selector: 'app-sender',
  standalone: true,
  imports: [CommonModule],
  template: `<button (click)="sendMessage()">Wyślij</button>`,
})
export class SenderComponent {
  constructor(private sharedService: SharedService) {}

  sendMessage() {
    this.sharedService.sendMessage('Wiadomość od nadawcy');
  }
}

Komponent odbierający:

// receiver.component.ts
import { Component, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedService } from './shared.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-receiver',
  standalone: true,
  imports: [CommonModule],
  template: `<p>Odebrane: {{ message }}</p>`,
})
export class ReceiverComponent implements OnDestroy {
  message: string;
  private subscription: Subscription;

  constructor(private sharedService: SharedService) {
    this.subscription = this.sharedService.message$.subscribe(
      message => this.message = message
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}