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ć się 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 previousValue
i currentValue
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 :)