Komponenty dynamiczne – czym one są cz. II

Komponenty dynamiczne ciąg dalszy: Angular 9 i drzewo dynamicznych komponentów

Ponad cztery lata temu opublikowaliśmy artykuł o komponentach dynamicznych w Angularze 2. Dziś jest już wersja Angulara 9 i chcielibyśmy naszą wiedzę „zaktualizować”.  Komponenty dynamiczne – czym one są? Jakie opcje zapewnia Angular w przypadku tworzenia komponentów dynamicznie? Co to jest i jak zbudować drzewo komponentów dynamicznych? W tym artykule poszukamy odpowiedzi na te pytania.

Komponenty dynamiczne

Jest już mnóstwo świetnych artykułów na temat komponentów dynamicznych. Krótko mówiąc, komponent dynamiczny to komponent, którego selektor nie jest użyty w żadnym template innego komponentu. Zamiast tego, jest ładowany imperatywnie poprzez swoją klasę i musimy podjąć dodatkowy wysiłek, który dla zwykłych komponentów wykonywany jest za kulisami przez sam framework.

O jakim wysiłku mowa?

Załóżmy, że już napisaliśmy nasz komponent. Teraz jednym ze sposobów załadowania go dynamicznie będzie:

1. Wstrzyknięcie ComponentFactoryResolver do komponentu, który będzie dany komponent dynamiczny ładował (nazwijmy go „loaderem”)

2. Stare podejście z Angulara 8 (zostanie “może” usunięte wraz z Angularem 11):
Dodanie komponentu  dynamicznego do tablic declarations i entryComponents modułu, który go używa. Następnie, stworzenie fabryki komponentu dynamicznego, przekazując typ komponentu do metody resolComponentFactory instancji ComponentFactoryResolver.

Nowe podejście z Angulara 9:
Właściwie nie musisz nigdzie deklarować komponentu, jego kod nie znajdzie się w inicjalnym bundle’u, ale przez brak modułu nie będziemy mogli skorzystać z przydatnych rzeczy np. z CommonModule (istnieje jednak sprytne obejście, więcej na ten temat w dalszej części).Aby uzyskać fabrykę komponentu, użyj składni dynamicznego importu określającej ścieżkę do modułu / pliku typescriptowego, w którym eksportowany jest komponent dynamiczny. Ten import zwróci Promise z modułem, w którym pod kluczem nazwy komponentu jest jego świeżo załadowana klasa. Następnie możemy przekazać go do metody resolveComponentFactory.

3. Kolejną rzeczą będzie znalezienie miejsca w widoku dla naszego dynamicznego komponentu. Do tego potrzebujemy referencji do ViewContainera. Taką referencję możemy uzyskać najpierw dodając do template’u naszego „loadera”. A później, odczytując wskazany “template reference variable” za pomocą ViewChild’a z określeniem, że chcemy ją “czytać” jako ViewContainerRef:

4. Wreszcie, gdy użyjemy metody create na ViewContainerze przekazując naszą fabrykę komponentu, nasz komponent dynamiczny zostanie stworzony. Metoda create zwraca również referencję do naszego nowo utworzonego komponentu, którą warto przechowywać jako zmienną – poprzez nią mamy dostęp do propsów komponentu lub możemy jej użyć do jego zniszczenia.

Cóż… wszystkie te kroki są dość uciążliwe. Czy Angular zapewnia prostszy, lepszy sposób?

Prostszy, tak, ale też bardziej ograniczony, co czyni go tylko sytuacyjnie lepszym

NgComponentOutlet

Jest to dyrektywa strukturalna, która ukrywa złożoność wyżej wymienionych kroków. Jest używana w następujący sposób:

Określamy, który komponent ma zostać załadowany według jego klasy – w Angular 9 wciąż musimy wykonać dynamiczny import, aby załadować chunk z tą klasą. Dodatkowo możemy przekazać niestandardowy injector (domyślnie jest brany z viewContainer’a na którym jest nałożona dyrektywa), a także transkludować jakiś kontent do ng-contentu komponentu dynamicznego.

Więc… jakie są wady tego rozwiązania?

Głównie dwie:

  • Nie mamy dostępu do referencji komponentu.
  • Nie mamy możliwości bindowania inputów / outputów.

Aby przekazać jakieś dane, musielibyśmy wstrzyknąć serwis do komponentu dynamicznego, albo, bardziej bezpośrednio, przekazać injector z injectionToken’em zawierającym te dane

Drzewo dynamicznych komponentów

Aby pokazać bardziej złożony przypadek użycia komponentów dynamicznych,
zastanówmy się nad pewnym problemem:

Wyobraź sobie, że nasza aplikacja jest builderem, w którym użytkownik tworzy jakąś strukturę ze wstępnie zdefiniowanych elementów. Niektóre z tych elementów mogą zawierać kolejne elementy, a niektóre z kolejnych kolejne (itd.). Ponadto, tę strukturę można zapisać po stronie serwera, a następnie pobierać z API i wyświetlać z powrotem użytkownikowi.

Przejdźmy przez rozwiązanie trochę uproszczonej wersji tego problemu:

  • mamy „zdefiniowane elementy”, jakąś pulę dynamicznych komponentów
  • istnieje obiekt przechowujący konfigurację dynamicznego komponentu, konfiguracja przechowuje typ komponentu dynamicznego i ew. konfiguracje zagnieżdżonych komponentów (taką postać potencjalnie miałby JSON ładowany z backendu)
  • na podstawie tego obiektu musimy załadować i wyświetlić drzewo
    Angularowych dynamicznych komponentów

Najpierw stwórzmy interface na ten obiekt konfiguracji:

To rekurencyjna struktura danych, która zawiera dwa klucze:

  • content, tablica konfiguracji, jeśli nasz komponent dynamiczny ma dzieci
  • type, który jest enumem wszystkich możliwych stringów opisujących typ komponentu

Mamy cztery typy komponentów dynamicznych, które później stworzymy: cmp1, cmp2 … itd.
Teraz stworzymy sobie utila z metodą zwracającą zamockowany obiekt powyższego interfejsu

Z tego obiektu będziemy musieli stworzyć drzewo dynamicznych komponentów i wyświetlić je użytkownikowi. Widzimy na przykład, że root component jest typu „cmp1” i ma dwoje dzieci: oba typu „cmp2”.

Boilerplate

Wygenerujmy boilerplate dla komponentów od cmp1 do cmp4. W tym samym pliku co komponent, dodajmy jeszcze @NgModule, w którym zadeklarujemy komponent i zaimportujemy moduły, komponenty, dyrektywy itp., których ten komponent używa. Moduł nie musi (i nie powinien) być eksportowany. Zostanie on odnaleziony i skompilowany, bo istnieje dynamiczny import wskazujący na plik, w którym ten moduł (i komponent) jest zawarty. Z każdego pliku z dynamicznym komponentem i modułem powstanie nam osobny chunk, ważnym jest dla nas to, że nie został oznaczony jako initial:

Drzewo dynamicznych komponentów - bolilerplate

Co więcej, Angular sam zajmie się dociągnięciem zależności, nie dublując importów istniejących w już załadowanych chunkach. W praktyce oznacza to, że powinniśmy mieć w naszej aplikacji nawet tysiąc dynamicznych komponentów, ale użytkownik załaduje tylko te, które są obecnie wykorzystywane na widoku.

SharedModule

W SharedModule przetrzymujemy wszystkie wspólne rzeczy używane przez nasze dynamiczne komponenty:

Następnie tworzymy obiekt, który będzie mapował stringi z konfiguracji na funkcje zwracające odpowiadające ścieżki do komponentów:

Ładowanie dynamicznych komponentów

Po zakończeniu naszych deklaracji przechodzimy do następnej części – ładowania naszych dynamicznych komponentów.

Użyjmy do tego dyrektywy strukturalnej, podobnej do ngComponentOutlet, ale bardziej dostosowanej do naszych potrzeb. Powinna być zadeklarowana i wyeksportowana w SharedModule:

Zasadniczo jest to procedura podobna do opisanej na początku tego tekstu, z tym że jest sparametryzowana za pomocą inputu z konfiguracją. W oparciu o type z konfiguracji uruchamiamy odpowiednią funkcję z importem w dynamicComponentImportsMap. Następnie, aby znaleźć klasę komponentu, szukamy pierwszej wartości importowanego modułu, która ma property „ɵcmp” (w Angular 9, do tej postaci jest transformowany dekorator komponentu). Jedyne co pozostało niewyjaśnione to DynamicComponentBase. Jest to klasa abstrakcyjna, która będzie nam służyć jako wspólne API naszych dynamicznych komponentów. Na razie zaimplementujmy tam jeden input na tablicę konfiguracji.

Nasze komponenty będą po niej dziedziczyć, aby mieć dostęp do konfiguracji:

Pozostaje jeszcze ostatni krok, aby faktycznie użyć naszej dyrektywy DynamicComponentLoader. W głównym komponencie musimy dodać:
app.component.ts

app.component.html

I w naszych dynamicznych komponentach, w celu rekursywnego ładowania potencjalnych dzieci:
cmp1.component.html

Struktura ostateczna

Po dodaniu stylów, nasza struktura prezentuje się tak:

struktura

Cały kod dostępny tutaj.

W następnym artykule zajmiemy się bardziej zaawansowanymi tematami – zarządzaniem inputami / outputami, skalowalnością i naprawimy pewien problem UX-owy / optymalizacyjny obecnego rozwiązań.

One Comment

  1. Pingback: ANGULAR 2 Komponenty dynamiczne cz I. - Angular.love

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *