Cześć po długim czasie! W związku z prywatnymi projektami, zawiesiłem bloga na okres wakacji – ale teraz jest plan wrócić ze zdwojoną siłą 😉
Ten wpis poświęcę dyrektywom strukturalnym, temat jest słabo opisany przez dokumentację Angulara, więc myślę, że zagłębienie tego tematu to dobry pomysł. Framework Angular udostępnia nam już parę wbudowanych, strukturalnych dyrektyw. np. ngFor, ngIf lub ngSwitch. Na pewno są Ci dobrze znane.
Dyrektywy strukturalne
Zatem zdefiniujmy najpierw pojęcie dyrektywy strukturalnej:
Dyrektywa strukturalna to dyrektywa, która dodaje / usuwa elementy drzewa DOM.
Spróbujemy stworzyć własną dyrektywę strukturalną. Zapewne nie raz trafiłeś już na irytujące Pop-Upy… przeglądasz stronę, a po 10 sekundach wyskakuje Ci reklama. Więc proponuję zrobić taką irytującą dyrektywę, która pokaże nam popup po zadanym czasie. Nasza dyrektywa będzie również sparametryzowana – będziemy mogli przekazać czas i kolor tła. Będzie to działać jak na poniższym filmiku:
Zwróć uwagę, jak pojawiają się kolejne węzły drzewa DOM (widok w narzędziach deweloperskich po prawej). W większości wypadków, aby usuwać lub dodawać węzły, wystarczy nam dyrektywa *ngIf. Jednak zdarzają się bardziej skomplikowane sytuacje, kiedy chcemy stworzyć własne dyrektywy, dodające nam węzły do DOM.
Spójrzmy jak wygląda HTML do powyższego filmiku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<div class="pop-up" *popup="2000;color: 'blue'"> <p>YOU WON ONE BILLION DOLLARS $$$!</p> </div> <div class="pop-up" *popup="5000;color: 'green'"> <p>YOU WON TWO BILLION DOLLARS $$$!</p> </div> <ng-template [popup]="8000" [popupColor]="'black'"> <div class="pop-up"> <p>YOU WON THREE BILLION DOLLARS $$$!</p> </div> </ng-template> <div class="pop-up" *popup="12000"> <p>YOU WON FOUR BILLION DOLLARS $$$!</p> </div> |
Każdy z elementów „div” to nasz kolejny pop-up, który pokazuje się po określonym czasie, nie ma żadnych *ngIfów!. Na każdy z tych elementów jest nałożona dyrektywa strukturalna *popup, która jako parametr przyjmuje czas oraz kolor tła. Trzeci div, dla odmiany jest zadeklarowany poprzez tag <ng-template>.
Gwiazdka (Asterrix), którą widzisz przed nazwą dyrektywy, to tzw. sugar syntax, pozwala nam na skrócony zapis, dzięki temu zapisowi nie musimy pisać <ng-template …>, Angular zrobi to za nas, pod maską, w następujący sposób:
1 2 3 |
<div class="pop-up" *popup="12000"> <p>YOU WON FOUR BILLION DOLLARS $$$!</p> </div> |
Powyższy kod jest interpretowany jako:
1 2 3 4 5 |
<ng-template [popup]="12000"> <div class="pop-up"> <p>YOU WON THREE BILLION DOLLARS $$$!</p> </div> </ng-template> |
Skoro już znamy różnicę pomiędzy deklaracją strukturalnej dyrektywy poprzez ng-template oraz gwiazdkę, to nauczymy się teraz, jak przygotować właśnie taką dyrektywę, która będzie obsługiwać zaprezentowaną logikę.
Kod dyrektywy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Directive({ selector: '[popup]' }) export class AnnoyingPopupDirective implements OnInit { @Input() popup = 1000; @Input() popupColor = 'pink'; constructor(private viewContainerRef: ViewContainerRef, private templateRef: TemplateRef<any>) {} ngOnInit() { setTimeout(() => { const embeddedView = this.viewContainerRef.createEmbeddedView(this.templateRef); embeddedView.rootNodes[0].style['background-color'] = this.popupColor; }, this.popup); } } |
Pracę wykonujemy w następujących krokach:
1) Deklaracja selektora dyrektywy – w naszym przypadku [popup]
2) Wstrzyknięcie do konstruktora klasy ViewContainerRef.
ViewContainerRef jest kontenerem, do którego możemy wrzucać np. dynamiczne komponenty, lub dodawać templatki. Co ważne, dodawane elementy nie są wrzucane do środka kontenera, natomiast są dodawane jako kolejni bracia tego kontenera, możemy powiedzieć, że są podwieszane do kontenera. Jeśli zajrzysz do API ViewContainerRef, to zauważysz, że możemy również czyścić kontener lub dodawać elementy na zadanych indeksach.
Nasz ViewContainerRef zostanie umiejscowiony na widoku aplikacji dokładnie w miejscu, gdzie występuje element z dyrektywą, do której go wstrzyknęliśmy. Czyli w naszym przypadku, utworzą się 4 kontenery, ponieważ stworzyliśmy 4 divy z 4 przypisanymi dyrektywami [popup].
3) Wstrzyknięcie do konstruktora klasy TemplateRef
TemplateRef reprezentuje jakiś kawałek kodu HTML (templatkę). Dostęp do templatki uzyskujemy poprzez wstrzyknięcie TemplateRef do konstruktora dyrektywy, a następnie nałożenie tej dyrektywy na pożądany element HTML.
W naszym przypadku, TemplateRef wskazuje dokładnie na ten kod HTML:
1 2 3 4 5 |
<ng-template [popup]="8000" [popupColor]="'black'"> <div class="pop-up"> <p>YOU WON THREE BILLION DOLLARS $$$!</p> </div> </ng-template> |
4) Kolejno, w hooku ngOnInit widzimy:
1 |
const embeddedView = this.viewContainerRef.createEmbeddedView(this.templateRef); |
Metoda createEmbddedView pozwala nam stworzyć instancję EmbeddedView na bazie wskazanej templatki. Templatkę przekazujemy jako parametr. Metoda ta, stworzy widoczek z templatki i załączy go do kontenera (ViewContainerRef).
SetTimeout i zmiana koloru powinna być dla Ciebie jasna, stąd nie będę wnikać w ten kod.
5) Przekazanie Paremetrów.
Dodajemy @Inputy. Teraz kluczowa kwestia do zapamiętania:
1 |
@Input() popupColor = 'pink'; |
1 2 3 |
<div class="pop-up" *popup="5000;color: 'green'"> <p>YOU WON TWO BILLION DOLLARS $$$!</p> </div> |
Zwróć uwagę, że Input nazywa się popupColor a przekazujemy parametr o nazwie color:
1 |
*popup="5000;color: 'green'" |
Jeśli używasz składni z Asterrixem (Gwiazdką), to nazwę parametru musisz poprzedzić nazwą selektora! Stąd parametr to color, ale Input to popupColor.
Natomiast pierwszej przekazanej wartości (czas, 5000), nie poprzedzam nazwą Inputa „popup”, ponieważ Input nazywa się tak samo jak selektor, stąd wpisuje od razu wartość 5000.
Jeśli nasza dyrektywa strukturalna ma więcej parametrów, to oddzielamy je kolejno średnikami:
1 |
*popup="5000;color: 'green'; height: 600; width: 500; isAnimated: false" |
Dobra…ale gdzie jest kod odpowiadający za ukrycie naszych elementów na starcie? Jak wspomniałem Assterix (*) to sugar syntax na dyrektywę ng-template. Kontent osadzony w ng-template domyślnie nie jest renderowany i jest to zamierzone zachowanie. Zatem:
1 2 3 |
<ng-template> <p>Hello World</p> </ng-template> |
Nie pokaże nic nam na ekranie. Natomiast jak dodamy ngIf = true:
1 2 3 |
<ng-template [ngIf]="true"> <p>Hello World</p> </ng-template> |
To kontent magicznie się pokazuje. Dzięki temu zachowaniu, nasze div’y z dykrektywami *popup są domyślnie ukryte, a pokazują się dopiero po wywołaniu metody createEmbeddedView().
Podsumowując działanie:
- dyrektywa tworzy kontener z elementu
- następnie do kontenera ładuje templatkę (ten element i jego kontent) po zadanym czasie w setTimeout
- czas i tło jako parametry
Oczywiście naszą dyrektywę można doszlifować, np. w rogu dodać „X”, który będzie czyścił widoki w kontenerach.
PODSUMOWANIE
W tym artykule pokazałem prosty przykład stworzenia strukturalnej dyrektywy. Jeśli spojrzymy jak mocno jest rozbudowane API ViewContainerRef / TemplateRef / EmbeddedView, to możliwości są naprawdę potężne. Jeśli chcesz zagłębić wiedzę z tworzenia strukturalnych dyrektyw, to polecam zagłębić się w kod dyrektywy *ngFor.
niesamowite jestem pod wrazeniem
Pingback: Real Live Case – Dyrektywa do obsługi ról – Angular.love