Tworzymy dynamiczne komponenty
Angular 2 umożliwia nam dynamiczne tworzenie komponentów. Jest to bardzo przydatna funkcjonalność, np. w przypadku tworzenia modali lub w każdym przypadku, kiedy chcemy stworzyć komponent w locie. W tym artykule pokażę jak tworzyć dynamicznie komponenty za pomocą serwisu ComponentFactoryResolver oraz jak się z nimi komunikować z poziomu kontrolera rodzica.
Zagadnienia, które poruszę:
- dynamiczne tworzenie komponentu za pomocą serwisu ComponentFactoryResolver
- umiejscowienie dynamicznego komponentu w dowolnym miejscu templatki rodzica
- przekazanie wartości do dynamicznego komponentu oraz wywoływanie jego metod
- dodanie dynamicznego komponentu do entryComponents
Każda część artykułu jest zakończona przykładem na Plunkerze. Do ostylowania przykładów posłużyłem się bootstrap 4.
TWORZYMY WZÓR DYNAMICZNEGO KOMPONENTU
Zaczniemy od napisania klasy, która posłuży nam za wzór naszego dynamicznego komponentu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Component({ selector: 'dynamic-component', template: ` <div class="card card-block"> <h4 class="card-title">Dynamic-Component</h4> <p class="card-text">I am dynamically created component. Don't forget to add me to EntryComponents in ngModule!</p> </div>` }) export class DynamicComponent implements OnInit, OnDestroy { ngOnInit(): void { console.log('Dynamic Component has been initialized') } callMeFromParent() : void { console.log('Hello, i am method of dynamic component') } ngOnDestroy(): void { console.log('I have been destroyed!'); } } |
COMPONENT FACTORY RESOLVER
W celu stworzenia dynamicznego komponentu w komponencie rodzica, zaczniemy od wstrzyknięcia serwisów ComponentFactoryResolver oraz ViewContainerRef do konstruktora rodzica (parent.component.ts):
1 2 |
constructor(private componentFactoryResolver: ComponentFactoryResolver, private viewContainerRef: ViewContainerRef) {} |
Oraz stworzymy metodę, która będzie przywołana na kliknięcie buttona, a efektem kliknięcia, będzie dynamicznie stworzenie komponentu.
1 2 3 4 5 |
createDynamicComponent() : void { const factory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent); const componentRef = this.viewContainerRef.createComponent(factory); componentRef.changeDetectorRef.detectChanges(); } |
Przenalizujmy, co się wydarzyło w naszej metodzie createDynamicComponent()
const factory:
- do consta przypisaliśmy wartość, którą zwraca nam metoda resolveComponentFactory, którą udostępnia nam serwis componentFactoryResolver
- Metoda resolveComponentFactory przyjmuje wyłącznie jeden parametr, komponent który ma stworzyć. Zatem jako parametr przekazaliśmy nazwę klasy naszego dynamicznego komponentu. Zwrócona wartość jest typu ComponentFactory.
const componentRef:
- wołamy metodę createComponent serwisu ViewContainerRef, który reprezentuje kontener, do którego możemy załączyć jeden lub więcej widoków.
- Kontener może zawierać dwa typy widoków (host views oraz embedded views). W naszym przypadku otrzymamy Host View, który powstaje podczas tworzenia instancji komponentu poprzez metodę createComponent.
- Nasz kontener widoków domyślnie wskazuje na nasz parent component, więc nasz stworzony dynamicznie komponent, doda się na koniec drzewa DOM widoku parenta.
- Metoda createComponent może przyjmować 4 parametry, w tym jeden konieczny, czyli fabrykę komponentu o typie ComponentFactory, która szczęśliwie jest trzymana w naszym const factory. Drugi parametr do index, czyli pozycja stworzonego komponentu w kontenerze. W przypadku braku podania indeksu, nowy widok zajmie ostatnią pozycję w kontenerze.
- Metoda createComponent zwraca typ ComponentRef. ComponentRef reprezentuje instancję komponentu stworzonego poprzez ComponentFactory. ComponentRef umożliwi nam dostęp do pól i metod publicznych naszego dynamicznego komponentu, co przyda nam się później przy przekazywaniu wartości.
componentRef.changeDetectorRef.detectChanges():
- jeśli zajrzymy do dokumentacji ComponentRef, widzimy, że implementuje changeDetectorRef, z kolei ten, udostępnia nam metodę detectChanges(). Ta metoda sprawdza changeDetectora i jego dzieci, ale to już temat na odrębny artykuł. W skrócie, wołamy tą metodę aby angular uruchomił lifecycle hooks dynamicznego komponentu oraz mechanizmy detekcji.
Sprawdźmy, czy wszystko działa, live example poniżej:
Wszystko cacy! Z tym, że generowanie komponentu, do którego nie przekazaliśmy żadnych danych i dodatkowo nie określiliśmy gdzie ma się pojawić w drzewie DOM, nie zawiele nam daje.
OKREŚLAMY MIEJSCE W DRZEWIE DOM DYNAMICZNEGO KOMPONENTU
Aby określić miejsce w drzewie DOM, gdzie pojawi się nasz dynamiczny komponent, musimy zmienić kontekst wywołania metody createComponent. Użyjemy do tego dekoratora viewChild(), który umożliwi nam dostęp do local variable w templatce parent component. Użycie local variable da nam referencje do komponentu lub po prostu selektora, np. diva.
Modyfikujemy nasz plik parent.component.html:
1 2 3 4 5 6 7 8 9 10 11 12 |
<div class="row"> <div class="col-xs-12"> <button (click)="createDynamicComponent()" class="btn btn-primary">Create dynamic component</button> </div> </div> <div class="row"> <div class="col-xs-12 dynamic-cmp-container"> <p>All dynamic components belong to the div with class dynamic-cmp-container</p> <template #dynamicComponentContainer></template> </div> </div> |
Czemu angularowy element template zamiast po prostu DIV? w przypadku przypisania local variable do diva, nasz dynamiczny komponent i tak opuści tego diva podczas renderowania i doda się zaraz za nim. W przypadku wątpliwości, proszę sprawdzić.
Łapiemy refrencję do #dynamicComponentContainer poprzez ViewChild() w parent.component.ts:
1 |
@ViewChild('dynamicComponentContainer', {read: ViewContainerRef}) dynamicComponentContainer; |
Zakładam, że dekorator @ViewChild jest Ci znany, ale metadata property „read”, być może nie.
Poprzez {read: someType}, określasz, jaki typ powinien zwracać local variable określony poprzez #localVariableName.
Jeśli nie określisz property read, to @ViewChild zwraca:
- ElementRef jeśli #localVariableName nie został zastosowany na komponencie
- instancję komponentu jeśli #localVariableName został zastosowany na komponencie
- jeśli chcesz inny typ, np ViewContainerRef, musisz o tym powiedzieć używać metadata property „read”.
Podsumowując, dzięki „read” możemy zmienić typ, który przetrzymuje @ViewChild, na np. ViewContainerRef, którego posiada potrzebną nam metodę createComponent().
Zmieniamy kontekst wywołania createComponent() z viewContainerRef na nasz dynamicComponentContainer.
1 2 3 4 5 |
createDynamicComponent() : void { const factory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent); const componentRef = this.dynamicComponentContainer.createComponent(factory); componentRef.changeDetectorRef.detectChanges(); } |
Zatem wiemy już, jak tworzyć dynamiczny komponent w porządanym przez nas miejscu. Poniżej link do plunkera po dodaniu #dynamicComponentContainer.
PRZEKAZUJEMY WARTOŚCI DO DYNAMICZNEGO KOMPONENTU
Do naszej templatki w dynamic.component.ts dodamy dwa publiczne pola, name oraz index (index będzie określał index w kontenerze – viewContainerRef).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Component({ selector: 'dynamic-component', template: ` <div class="card card-block"> <h4 class="card-title">Dynamic-Component</h4> <p class="card-text">I am dynamically created component. My name is {{ name }}, and my index in ViewContainerRef is {{index}}</p> </div>` }) export class DynamicComponent implements OnInit, OnDestroy { index: number; name: string; ... } |
Będziemy przekazywać wartości od rodzica, a nie ma @Input() ? Zgadza się, nie ma. Dynamiczne komponenty nie mają supportu inputa i outputa. Wróćmy do metody createDynamicComponent w parent.component.ts. Dopiszemy parę linijek.
1 2 3 4 5 6 7 8 9 |
createDynamicComponent() : void { const factory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent); const componentRef = this.dynamicComponentContainer.createComponent(factory); componentRef.instance.name = "John Doe"; const index = this.dynamicComponentContainer.indexOf(componentRef.hostView); componentRef.instance.index = index; componentRef.instance.callMeFromParent(); componentRef.changeDetectorRef.detectChanges(); } |
Skupmy się na:
1 |
componentRef.instance.name = "John Doe"; |
Jak wcześniej pisałem, componentRef trzyma wartość typu ComponentRef, która przetrzymuje instancję dynamicznego komponentu. Jeśli zajrzymy do dokumentacji klasy ComponentRef, widzimy property Instance. Ta właściwość daje nam dostęp do pól i metod publicznych naszego dynamicznego komponentu. Dzięki temu możemy łatwo przekazać property name do dynamicznego komponentu z komponentu rodzica lub zawołać metodę:
1 |
componentRef.instance.callMeFromParent(); |
Jak już się dowiedzieliśmy, nasze dynamiczne komponenty są trzymane w kontenerze viewContainerRef, na konkretnych pozycjach. Możemy łatwo odczytać taki indeks. Klasa ComponentRef, udostępnia nam także property hostView, które reprezentuje konkretne viewRef w Host View. Natomiast serwis ViewContainerRef udostępnia nam metodę indexOf(viewRef), która zwraca nam indeks przekazanego w parametrze viewRef. Łącząc dostępne informacje, złapiemy indeks dynamicznego komponentu w kontenerze:
1 |
const index = this.dynamicComponentContainer.indexOf(componentRef.hostView); |
Posiadając indeks dynamicznego komponentu, możemy go np usunąć, przesunąć na inne miejsce, lub wywołać metodę tylko na konretnym dynamicznym komponecie.Polecam się zapoznać z całym api ViewContainerRef, ponieważ udostępnia wiele ciekawych metod, które pozwolą nam manipulować elementami, które przetrzymuje. Poniżej live example podsumowujący cały artykuł.
… i ostatnia rzecz, bez której nasz kod nie ruszy.
Z racji, że dynamiczny komponent pojawia się w locie, musimy go zadeklarować jako entry component w ngModule, a nie wyłącznie w declarations. W pliku module.app.ts musi być:
entryComponents: [DynamicComponent],
Dokumentacja angulara na temat entry Components mówi:
If your app happens to bootstrap or dynamically load a component by type in some other manner, you’ll have to add it to
entryComponents
explicitlyJeżeli chcesz wiedzieć więcej o komonentach dynamicznych w Angular 9 kliknij tutaj.
Ciekawy artykuł chociaż tak samo jak cała reszta artykułów w necie pokazuje tylko 1 poziomowe tworzenie komponentów.
Bardzo często zachodzi potrzeba że będzie trzeba zrobić zagnieżdżone komponenty – np. całą dynamiczną sekcję z dynamicznymi komponentami. Co więcej będzie trzeba mieć jakoś do nich dostęp. I co wtedy?
Mówiąc zagnieżdżone komponenty, masz na myśli, że nasz dynamiczny komponent zawiera inne komponenty statyczne czy kolejne dynamiczne?
Trochę to dziwne, nazywamy dynamicznym komponentem (selector: 'dynamic-component’) coś co wklejamy do innego komponentu?? To raczej nie ten rodzic powinien nazywać się dynamicznym?? a z drugiej strony co to za dynamiczność jeśli w kodzie i tak mamy 'zafiksowany’ selektor do wyświetlenia. Raczej jest to inny sposó pokazania .
Jak ja widze dynamiczny komponent?? W parametrze podaje templatke albo konfiguracje co ma się pojawić w
Bardzo przydatny artykuł szkoda, że plunker z tym kodem nie działa.
dzieki! niestety plunker dał ciała z podbijaniem wersji i wszystkie przyklady pisane 2 lata temu nie działają, będę musiał przepisać je na stackblitz
Szukałem właśnie sposobu na zrobienie własnego snack-bara. Czy w ten sposób można wyświetlić X dynamicznych komponentów? np. jest kilka powiadomień do usera i by wszystkie mu się pokazały
tak, jak najbardziej. Musisz puszować taki komponent do konterenera i po jakimś czasie czyścić go na zadanym indeksie, bo zakładam, że chcesz je kolejkować.
Tak, dokładnie, chcę kolejkować. Dzięki.
Dobra robota z blogiem.
btw. blog o angularze a czemu stoi na wordpresie 🙂 i https by się przydał 🙂
Pingback: Komponenty dynamiczne - czym one są cz. II - Angular.love