Komunikacja pomiędzy komponentami w Angular 2

Ten wpis jest 3 częścią z 5 w kursie Angular 2

Przed Wami trzecia część kursu Angular 2. W tym wpisie omawiam różne sposoby komunikacji pomiędzy komponentami na prostych przykładach.

Angular 2 nie jest jeszcze gotowy na produkcję. W momencie powstawania tego wpisu najnowszą wersją tego frameworka była Release Candidate 4. Jest to wersja prawie gotowa do wydania, jednak minie jeszcze trochę czasu zanim całość się ustabilizuje, a pomiędzy wersjami RC-1, RC-2 i RC-3 pojawiły się dość poważne zmiany (routing, formularze).

Interakcja pomiędzy komponentami

W poprzednich artykułach wspominałem o tym, że jest wiele sposobów na zrealizowanie komunikacji pomiędzy komponentami. Pokazałem również prosty przykład przekazywania danych od rodzica do dziecka przy pomocy dekoratora @Input. Jednak tak minimalistyczny przypadek rzadko zdarza się w rzeczywistości i te metody zdecydowanie nie są wystarczające do tworzenia rozbudowanych aplikacji internetowych! Na szczęście sam Angular 2 daje nam co najmniej kilka opcji, z których możemy skorzystać w celu przesyłania informacji pomiędzy komponentami. Możliwości jest wiele i każdą z nich opiszę po krótce.

@Input czyli komunikacja rodzica z dzieckiem

Działanie dekoratora @Input pokazywałem już w poprzednim artykule. Pozwala on na przesyłanie danych do komponentu. Jednak co w przypadku gdy chcemy dowiedzieć sie kiedy dokładnie przekazywane dane się zmieniają? Osobom znającym AngularJS nasuwa się pewnie $watch, jednak w Angularze 2 nie musimy już kombinować w ten sposób!

Zacznijmy od podstawowego kodu. Tworzymy komponent mający jedno pole z adnotacją @Input. W ten sposób z poziomu rodzica możemy przekazać dane do dziecka, jak to już opisywałem:

@Component({
  selector: 'my-child-component',
  template: `
    prop1: {{ prop1 }}
  `
})
export class MyChildComponent {  
  @Input() prop1:string;
}

Pierwszą metodą, aby być informowanym o modyfikacjach zachodzących w prop1, jest zamiana tej właściwości w parę setter/getter. Zamiast @Input() prop1:string; piszemy:

private _prop1;

@Input() set prop1(prop1:string) {
  this._prop1 = `${prop1} decorated!`;
}
get prop1() {  
  return this._prop1;
}

W momencie zmiany wartości pola prop1 zostanie wywołana funkcja. W tym przypadku wykorzystujemy ją do udekorowania przekazanej wartości poprzez dodanie do niej ciągu znaków “ decorated!”. Bardziej przydatnym przykładem mogłoby być np. ustawienie wartości domyślnej gdy prop1 jest puste.

Druga metoda to wykorzystanie wspomnianego w poprzednim wpisie zdarzenia cyklu życia ngOnChanges. W klasie komponentu implementujemy interfejs OnChanges i tworzymy metodę ngOnChanges:

ngOnChanges(changes: SimpleChanges) {  
  const prop2Changes:SimpleChange = changes['prop2'];
  if (prop2Changes) {
    console.log(`prop2 changed!`, changes['prop2']);
  }
}

Metoda ta przyjmuje obiekt opisany interfejsem SimpleChanges, w którym kluczami są zmieniające się właściwości, a wartościami są specjalne obiekty typu SimpleChange. W tym przypadku interesują nas zmiany właściwości prop2, więc jeśli takowe są to wyświetlamy informację przy pomocy console.log(…). Obiekt typu SimpleChange zawiera w sobie pola previousValuecurrentValue oraz metodę isFirstChange(). Więcej można doczytać w dokumentacji SimpleChange. Tutaj działający przykład komunikacji rodzica z dzieckiem:

@Output czyli komunikacja dziecka z rodzicem

Działanie dekoratora @Output pokazywałem już w poprzednim wpisie, więc nie chcę się tutaj rozwodzić. Dla formalności krótki przykład. W komponencie-dziecku tworzymy pole z adnotacją @Output do którego przypisujemy nową instancję EventEmitter. Następnie w odpowiednim momencie wywołujemy metodę emit na tej instancji, gdy chcemy powiadomić rodzica o zmianach:

@Output() onProp = new EventEmitter<string>();

onInput(value:string) {  
  this.onProp.emit(value);
}

Rodzic zać ustawia odpowiedni callback na komponencie-dziecku:

<my-child-component (onProp)="changed($event)"></my-child-component>  

Zobaczcie cały kod w interaktywnym przykładzie:

#ref czyli lokalna referencja na komponent-dziecko

W Angular 2 rodzic może stworzyć sobie lokalną referencję na własności klasy komponentu-dziecka. Wyobraźmy sobie, że mamy komponent, który wykonuje jakieś asynchroniczne operacje. Chcielibyśmy mieć możliwość kontrolowania tego komponentu z zewnątrz: zatrzymania jego pracy, wznowienia jej oraz odczytania postępu wyrażonego w procentach. Nic prostszego! W szablonie rodzica tworzymy lokalną referencję na komponent-dziecko i możemy korzystać ze wszystkich metod i własności klasy dziecka:

<my-child-component #child></my-child-component>

progress: {{ child.progress * 100 }}%  
<button (click)="child.start()">start</button>  
<button (click)="child.stop()">stop</button>  

Fragment kodu klasy komponentu-dziecka wygląda tak:

export class MyChildComponent {  
  progress = 0;

  start() {
    …
  }

  stop() {
    …
  }
}

Tutaj można zobaczyć cały kod prezentujący lokalną referencję na dziecko.

@ViewChild czyli referencja na dziecko w klasie rodzica

Zaprezentowana przed chwilą metoda świetnie sprawdzi się w prostych przypadkach, ma jednak jedno poważne ograniczenie: Cała logika związana z referencją na komponent-dziecko musi być zawarta w szablonie rodzica. Innymi słowy, klasa rodzica nie ma dostępu do dziecka. Problem ten można rozwiązać używając dekoratora @ViewChild w klasie rodzica. Aby dostać referencję na komponent-dziecko używamy tej adnotacji w podobny sposób jak @Input czy @Output:

export class ParentComponent {  
  @ViewChild(MyChildComponent) private childComponent:MyChildComponent;

  get progress():number {
    return this.childComponent.progress;
  }

  start() {
    this.childComponent.start();
  }

  stop() {
    this.childComponent.stop();
  }
}

Jeśli chcemy robić z komponentem-dzieckiem coś bardziej skomplikowanego to musimy poczekać na jego zainicjalizowanie. Mamy pewność, że tak się stało dopiero w zdarzeniu cyklu życia ngAfterViewInit.

Dla porównania prezentuję dokładnie ten sam przykład co w poprzednim akapicie, jednak zaimplementowany z użyciem @ViewChild:

Jeśli komponentów-dzieci jest kilka możemy skorzystać z dekoratora @ViewChildren, który pobiera kilka komponentów-dzieci i zwraca jako QueryList, który jest żywą obserwowalną kolekcją

Komunikacja przy pomocy serwisu

W bardziej rozbudowanych aplikacjach komunikacja pomiędzy rodzicem a dzieckiem to jedno, natomiast bardzo potrzebny jest również sposób na przesyłanie informacji pomiędzy komponentami, które są od siebie bardziej oddalone. W szczególności: Pomiędzy komponentami, które nie wiedzą gdzie wzajemnie znajdują się w strukturze aplikacji. Użycie do tego pośredniczącego serwisu jest bardzo uniwersalne. Dodatkowo możemy tutaj użyć biblioteki rxjs, z której korzysta zresztą sam Angular 2.

rxjs jest biblioteką implementującą reaktywne programowanie funkcyjne (FRP) w JavaScripcie. Jest to dość skomplikowany koncept wymagający zmiany myślenia o programowaniu, dlatego na potrzeby tego artykułu skorzystamy tylko z podstawowych możliwości rxjs. Więcej można doczytać w dokumentacji.

Przykładowy serwis zamieszczam poniżej. Publiczne pole data$ jest tym, co będą mogły obserwować komponenty (za pośrednictwem tego pola będą odbierać informacje). Metoda addData posłuży im zaś do przesyłania danych.

@Injectable()
export class DataService {  
  private dataSource = new Subject<string>();
  data$ = this.dataSource.asObservable();

  addData(value:string) {
    this.dataSource.next(value);
  }
}

Aby skorzystać z takiego serwisu tradycyjnie dodajemy go do tablicy providers w dekoratorze klasy, która jest wspólnym rodzicem komunikujących się ze sobą komponentów. Następnie wstrzykujemy go do konstruktorów komponentów-dzieci i używamy:

class MyChildComponent {  
  addData() {
    this.dataService.addData('data');
  }

  constructor(private dataService: DataService) {
    dataService.data$.subscribe((value:string) => {
      console.log('Data!', value);
    });
  }
}

Podobnie jak w poprzednich przypadkach, tutaj również wrzucam interaktywny przykład wykorzystania pośredniczącego serwisu z rxjs:

Podsumowanie

Opisałem kilka różnych sposobów komunikacji pomiędzy komponentami sugerowanych przez twórców Angular 2. Są to metody, które znajdą zastosowanie głównie w prostych przypadkach i raczej nie sprawdzą się do komunikowania się wielu komponentów. W celu zarządzania stanem całej aplikacji oraz przesyłania danych pomiędzy odległymi komponentami zastosowałbym raczej bibliotekę Redux. Koncept Reduksa już opisywałem, a jego konkretne zastosowanie w aplikacji Angular 2 znajdzie się w kolejnym wpisie, który już jest w trakcie powstawania 🙂

Nawigacja po kursie:
  1. Wstęp do Angular 2
  2. Własne komponenty w Angular 2
  3. Komunikacja pomiędzy komponentami w Angular 2
  4. Angular 2 i Redux
  5. Dependency Injection w Angular 2