Wybierz region
pl
  • PL
  • EN
Wydrukuj

Komunikacja kluczem do sukcesu, czyli jak dogadują się komponenty w Angularze?

Skuteczna komunikacja jest potężnym narzędziem pomagającym nam podczas całej przygody zwanej życiem – nie tylko w sferze prywatnej, ale również zawodowej. Umiejętność dobrego komunikowania się z innymi ludźmi pozwala nam sięgać po sukcesy i odblokowywać życiowe osiągnięcia. Dzięki właściwie ukierunkowanej wymianie informacji jesteśmy w stanie zbudować grono zaufanych znajomych, czy piąć się w górę na ścieżce kariery.

Nie jest to jednak umiejętność zarezerwowana jedynie dla organizmów żywych. Cóż by to była bowiem za aplikacja, której komponenty nie potrafiłyby komunikować się pomiędzy sobą? Wprawdzie hermetyzacja komponentów w Angularze sprawia, że nie są one świadome istnienia innych oprócz siebie, ale mimo wszystko muszą posiadać sposoby na porozumiewanie się, ponieważ czy jesteś w stanie wyobrazić sobie aplikację, w której nie dochodzi do żadnej wymiany danych pomiędzy różnymi jej elementami? W takim świecie przykładowo zalogowanie do aplikacji nie odblokowałoby dodatkowych funkcji dostępnym jedynie znanym użytkownikom, ponieważ komponenty nie byłyby świadome, że ktoś się zautoryzował.

Komunikacja Rodzic -> Dziecko

Zaczynając jednak od początku, najprostszym przykładem komunikacji między komponentami jest przekazywanie danych na poziomie rodzic – dziecko. Najbardziej klasycznym przykładem jest komponent prezentujący listę elementów. Podłużymy się tu prostą aplikacją, która była budowana we wcześniejszym wpisie dotyczącym pipe’ów (Pipe’y w Angularze).

Zawiera ona komponent CharactersComponent, którego zadaniem jest przedstawienie listy dostępnych postaci w formie małych kart. Kierując się zasadą, że komponenty powinny być podzielone na te, które posiadają logikę oraz te nieco głupsze, których nie interesuje, co do nich wpadnie, a jedynym zadaniem jest wyświetlenie otrzymanych danych, ten komponent powinien być de facto podzielony na dwa – jeden pobierający listę postaci i drugi wyświetlający tylko pojedynczy kafelek.

Kluczowym elementem nowego, głupiutkiego komponentu jest zadeklarowanie zmiennej, do której rodzic (czyli nasz komponent listy) będzie wstrzykiwał właściwe dane do wyświetlenia. Musimy tu pamiętać o umieszczeniu na początku magicznego słowa @Input(), które odznacza nic innego jak dane wejściowe komponentu. Po nim klasycznie dodajemy enkapsulację (public/private), nazwę zmiennej oraz wskazane jest zadeklarowanie typu – dodawanie typów zmiennych znacznie upraszcza dalszą pracę nad aplikacją i pomaga kontrolować poprawność wprowadzanych danych.

Należy jedynie pamiętać, że jeśli na tym poprzestaniemy, to nasz komponent wciąż nie będzie wiedział, co właściwie powinien wyświetlić, więc zaprezentuje nam jedynie puste karty.

Na szczęście skorygowanie tej usterki jest równie proste, ponieważ wystarczy w komponencie rodzicu w selektorze dziecka przekazać obiekt z danymi jako jego atrybut – pilnując, oczywiście, by nazwa atrybutu była zgodna z nazwą zadeklarowanej zmiennej.

Dzięki temu szybko wrócimy do punktu startu, czyli poprawnej prezentacji kafelków z właściwymi danymi.

Komunikacja Dziecko -> Rodzic

Niekiedy zachodzi potrzeba odwrócenia kierunku komunikacji i przekazania danych z wnętrza dziecka do jego rodzica, na przykład gdy klikniemy na element dziecka i chcemy rodzicowi powiedzieć, co właściwie wybraliśmy. Przykładowo chcielibyśmy w naszej prostej aplikacji umożliwić użytkownikowi sprawdzenie dokładniejszego opisu klasy wybranej postaci poprzez kliknięcie na jej nazwę. Opis moglibyśmy wyświetlić poniżej listy postaci, więc komponent go zawierający stanie się sąsiadem kafelków postaci. Komponenty kafelków i komponent opisu nie mają pojęcia o swoim wzajemnym istnieniu, ale mają wspólnego rodzica, dzięki czemu kafelek może przekazać komponentowi listy wybrany element, który  następnie zostanie wstrzyknięty do sąsiada.

W tym przypadku również musimy w komponencie dziecka zadeklarować zmienną, ale już nie wybranego przez nas dowolnego typu. Zaczynamy znów od magicznego hasła, ale tym razem @Output() oznaczającego dane wyjściowe, a po nim deklaracja zmiennej o typie EventEmitter, który sam w sobie jest typem generycznym, więc musimy mu wskazać, jaki typ faktycznie będzie przez niego przekazywany. Tym razem dodatkowo tą zmienną musimy zainicjalizować.

Nie możemy jednak na tym poprzestać, ponieważ gdybyśmy zostawili komponent w tej formie, to biedak wciąż by nie wiedział, co tak naprawdę ma wysłać do rodzica. W szablonie html musimy zatem mu wskazać, że na kliknięcie nazwy rasy powinno się coś zadziać.

W naszym przypadku nie zadzieją się wprawdzie żadne fajerwerki, ale ta metoda kryje w sobie bardzo ważny element, mianowicie wywołana jest w niej wewnętrzna metoda obiektu EventEmitter emit(), do której jako parametr przekazujemy wartość, którą chcemy przekazać do rodzica.

I tak jak w poprzedniej sekcji, musimy ponownie wrócić do naszego komponentu listy i wskazać mu, jak powinien się zachować, gdy dziecko wyemituje do niego informację. Jako że nie mamy tu do czynienia z żadną fizyką kwantową możemy wartość kryjącą się pod hasłem $event przypisać bezpośrednio do zmiennej selectedRace, która niżej wstrzykiwana jest bezpośrednio do komponentu opisu. Nic nie stoi, oczywiście, na przeszkodzie, by zamiast przypisania wywołać tutaj funkcję, której parametrem stanie się przekazywana wartość, wszystko jednak zależy od potrzeb aplikacji.

Bardzo ważne jest, by pamiętać o poprawnych nawiasach – nawiasy kwadratowe oznaczają wartości wchodzące do komponentów, czyli Inputy, natomiast nawiasy okrągłe są przypisane do wartości wychodzących, Outputów. Natomiast $event jest swego rodzaju placeholderem dla wartości, która zostaje przekazana. Warto tu zauważyć, że adekwatnie przekazywane są wszelkie eventy wynikające z interakcji z aplikacją, na przykład kliknięcie myszką.

I szybki rzut oka na uzyskany efekt:

Dla prostego zobrazowania co się właściwie zadziało między komponentami możemy się posłużyć poniższym wykresem. Czerwony bloczek oznacza komponent rodzica, czyli nasz komponent listy, pomarańczowe zaś to komponenty dzieci, sąsiadujące ze sobą, ale nieświadome egzystencji tego drugiego. Czerwone strzałki prezentują nam dane wstrzykiwane do dzieci (Input), zaś pomarańczowa jest informacją przekazywaną do rodzica (Output).

Komunikacja między sąsiadami

Sprawa się nieco komplikuje w momencie, gdy chcemy przekazać dane pomiędzy niepowiązanymi ze sobą komponentami. Załóżmy, że chcielibyśmy mieć możliwość również podejrzeć statystyki broni używanej przez bohaterów. Bronie nie są ściśle powiązane z postaciami, więc posiadają swój osobny moduł WeaponModule, który nie ma nic wspólnego z modułem prezentującym kafelki postaci. W takim przypadku nie możemy posłużyć się znanym już sposobem komunikacji, ale nie ma strachu, ponieważ z pomocą pędzą do nas serwisy.

Możemy sobie na nasze potrzeby utworzyć taki oto prosty serwis, którego obecnie jedynym zajęciem jest przekazanie wybranej broni do innych komponentów. Tutaj jednak nie będziemy bezpośrednio przekazywać obiektów, jak to zrobiliśmy w przypadku komunikacji Rodzic-Dziecko, ale posłużymy się strumieniami danych.

Strumienie dają tę przewagę, że mogą mieć wielu subskrybentów, a zatem posługując się takim strumieniem możemy pozwolić wielu komponentom na reagowanie na kliknięcie wybranej pozycji na kafelku.

Do wykorzystania tej metody potrzebujemy w serwisie trzech elementów:

  • Zmiennej zadeklarowanej jako Subject,
  • Metody selectWeapon do przypisania wartości do Subjectu,
  • Metody getSelectedWeapon propagującej strumień danych.

Po stronie komponentów wygląda to w następujący sposób:

Nasz główny komponent rodzic (CharactersComponent) otrzymuje od komponentu kafelka kolejną emitowaną wartość, którą tym razem jest kliknięta broń (emitowanie wartości w tym przypadku dzieje się tak samo jak było opisywane w przypadku rasy). Tym razem jednak zamiast przypisywać obiekt do zmiennej przekażemy go jako parametr do funkcji, w której następnie zostanie on przesłany dalej do serwisu. Koniecznie należy tu pamiętać o prawidłowym wstrzyknięciu serwisu do konstruktora komponentu!

Mając tak przygotowane podłoże możemy przeskoczyć od razu do modułu zajmującego się broniami i uzupełnić go o założenie subskrypcji (przyp. O subskrypcjach naskrobałam nieco w poprzednim artykule Asynchroniczność w Angularze i jak ją ugryźć, do którego serdecznie zapraszam):

Dzięki tak założonej subskrypcji komponent będzie reagował na każde kliknięcie w kafelkach postaci i zawsze prezentował ostatnio wybrany obiekt.

Ponownie możemy zobrazować aktualną sytuację wykresem, na którym od razu widać, że aplikacja nieco nam się rozrosła. Pojawił się dodatkowy serwis, który stał się pośrednikiem pomiędzy dwoma modułami, a komunikacja między nim a komponentami polega tym razem nie na przekazaniu obiektu, a raczej na wołaniu jego metod. Dodatkowo strzałka dwukierunkowa obrazuje tutaj wołanie metody zwracającej Observable, co de facto jest jednocześnie zapytaniem z odpowiedzią.

 

Opisywane tu przykłady nie są jedynymi sposobami zbudowania komunikacji pomiędzy komponentami. Można również posłużyć się narzędziami dostępnymi w bibliotece nawigacji Angulara - @angular/router – takimi jak RouteParams czy QueryParams. Unikamy wówczas potrzeby budowania dodatkowego serwisu, aczkolwiek temat nawigacji jest na tyle rozległy, że moglibyśmy pozostawić go sobie na inny dzień…

 

Bonus – rodzaje Subject’ów

Nie bez przyczyny w ostatniej metodzie w WeaponsComponent pojawił się console.log. Posłuży nam jeszcze do szybkiej prezentacji różnic pomiędzy rodzajami Subject’ów. Deklarując zmienną, która stanie się strumieniem danych mamy do wyboru trzy typy: Subject, RepeatSubject oraz BehaviorSubject. Każdy z nich ma inne dodatkowe cechy, które go wyróżniają, a to, którego wybierzemy, powinno być całkowicie zależne od naszych potrzeb.

By pokazać te różnice skorzystamy z dodatkowego komponentu WeaponItem, który w swoim ngOnInit ma założoną identyczną subskrypcję i jego zadaniem jest pokazać ostatnio wybraną pozycję, ale cały komponent inicjalizowany jest dopiero po wciśnięciu przycisku „Init other”.

Gdy stosujemy zwykłego Subject’a po zainicjowaniu nowego komponentu, zostanie on podłączony jako nowy subskrybent i będzie otrzymywał każdą kolejną wartość. Ergo, komponent nie będzie świadomy, że coś pojawiło się już wcześniej, więc w tym przypadku nie będzie w stanie zaprezentować nam poprzedniej wartości oraz w konsoli nie pojawią się żadne nowe wpisy:

Jeśli zależy nam na wcześniejszych wartościach możemy sięgnąć po jeden z pozostałych typów. Aczkolwiek mimo podobnego zachowania jest również między nimi bardzo znacząca różnica.

ReplaySubject deklarowany jest w taki sam sposób jak klasyczny Subject:

Zachowuje się jednak zdecydowanie inaczej, ponieważ można go porównać do bezmyślnej papugi, która będzie powtarzać wszystko, co usłyszy. I chociaż na pierwszy rzut oka wyświetli nam się na ekranie faktycznie ostatnia kliknięta pozycja, to widok konsoli mógłby nieprzygotowanych nieco zaskoczyć.

Znajomość angielskiego może nas jednak nieco przygotować, ponieważ ReplaySubject robi dokładnie to, co wskazuje jego nazwa – powtarza. W naszym przypadku w momencie gdy podepnie się nowy subskrybent natychmiastowo otrzymuje on wszystkie wartości, jakie zostały do tego momentu wyemitowane. O ile ma to swoje niezaprzeczalne zalety, o tyle może przyprawić o niemały ból głowy nieświadomych takiego zachowania.

Jeśli natomiast interesuje nas faktycznie ostatnia wartość jaka została wyemitowania, to możemy śmiało sięgnąć po trzeciego kandydata, jakim jest BehaviorSubject. Różni się on już na etapie inicjalizacji,  ponieważ wymaga by na starcie wskazać mu wartość początkową – co zdaje się być całkiem logiczne zważywszy na jego działanie. Nic oczywiście nie stoi na przeszkodzie, by tą wartością był zwykły null, co w naszym przypadku jest dość adekwatne, biorąc pod uwagę, że na starcie aplikacji użytkownik nie zdąży jeszcze nic kliknąć.

I jak możemy zauważyć „Last selected” ponownie pokazuje ostatnio wybraną pozycję, natomiast w konsoli widzimy, że tylko ostatni obiekt został powtórzony. Można więc pokusić się o stwierdzenie, że BehaviorSubject jest taką wytresowaną papugą, która jest nauczona, że ma powtarzać tylko jedną wartość (albo może to papuga ze sklerozą, która nie pamięta, co było wcześniej?...)

Gdyby zatem ktoś ośmielił się zapytać, który z Subject’ów jest lepszy, to jedyną w pełni poprawną odpowiedzią będzie… To zależy. A zależy od potrzeb danej aplikacji. Jeśli nie interesuje nas, co się działo wcześniej i chcemy tylko i wyłącznie najnowsze wartości, to śmiało możemy wybrać klasyczny Subject. Jeśli zaś interesuje nas przeszłość i potrzebujemy wcześniejszych wartości, wybieramy jedną z naszych papug.


Paulina Schoenfeld

Full Stack Developer, za czasów studiów sceptycznie nastawiona do programowania, aktualnie fanka Angulara i frontendu, który okazał się być naprawdę fajną i satysfakcjonującą zabawą.


Wydrukuj