W tym artykule dowiesz się, czym jest Directive composition API, dlaczego jego wypuszczenie na świat trwało tak długo, jakie rozwiązuje problemy oraz jakie są jego zalety i wady.
Czym więc jest Directive composition API? Jest to funkcjonalność, która umożliwia łączenie dyrektyw z innymi dyrektywami i… to by było na tyle! Wydaje się proste, prawda? Moglibyśmy rozmyślać, czy ta funkcjonalność nie powinna być dostępna już we wcześniejszych wersji Angulara? To pytanie pojawia się już od jakiegoś czasu, a dokładniej od 23 maja 2016 roku, kiedy to powstała pierwsza wzmianka na github na ten temat, a data ta zbiega się z publikacją pierwszego release’a Angular 2, która miała miejsce w maju 2016 roku.
Dlaczego trwało to tak długo?
Temat był bardzo popularny wśród społeczności angularowej, a prawie 1,5 roku po stworzeniu issue pojawiło się światełko w tunelu. Twórca angulara zapowiedział, że po wprowadzeniu kompilatora Ivy będzie to wykonalne.
Po wprowadzeniu Ivy w Angular 8, społeczność odświeżyła wątek i ponownie poprosiła o informacje na temat przyszłych planów dotyczących tej funkcjonalności, a kilka miesięcy później otrzymali je – realizacja takiej funkcji wymagałaby poważnej refaktoryzacji ze względu na niekompatybilne struktury danych. W końcu, 13 listopada 2020 roku, Directive composition API została dodana do oficjalnej roadmapy. Wraz z 15 wersją Angulara ta długo oczekiwana funkcjonalność została wreszcie wprowadzona.
Jakie problemy rozwiązuje Directive composition?
Spójrzmy na poniższy przypadek użycia. Wyobraźmy sobie, że mamy trzy komponenty
współdzielą one pewną funkcjonalność – zmiana kolorów (wszystkich z nich), ustawienie stanu disabled (button i toggle). Najprostszym sposobem na osiągnięcie tego jest wykorzystanie
Inputów dla każdego z komponentów.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Component({ selector: 'app-button', standalone: true, template: `<button> <ng-content></ng-content> </button>`, styleUrls: ['./button.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ButtonComponent { @Input() color: Color = 'primary'; @Input() disableState = false; @HostBinding('class') get hostClasses() { return { [`${this.color}`]: true, ['disabled']: this.disableState, }; } } |
Takie podejście narusza zasadę DRY, więc zastanówmy się nad innym podejściem.
Jednym z możliwych rozwiązań jest wykorzystanie dziedziczenia. Możemy utworzyć komponent bazowy BaseComponent, który będzie rozszerzał każdy następny komponent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Component({ selector: 'app-base', standalone: true, templateUrl: ''./base.component.html', styleUrls: ['./base.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BaseComponent { @Input() color: Color = 'primary'; @Input() disableState = false; @HostBinding('class') get hostClasses() { return { [`${this.color}`]: true, ['disabled']: this.disableState, }; } } |
W takim przypadku komponent spinner będzie obsługiwał stan disabled, co jest niewłaściwe i ogólnie rzecz biorąc nie jest to najlepsza praktyka, by udostępniać API, które nie będzie wykorzystywane.
Aby to wyeliminować, możemy utworzyć dwie dyrektywy – dyrektywę Disable i dyrektywę Color. W ten sposób nasz kod będzie podzielony dla każdej funkcjonalności.
1 2 3 |
<app-button appColor appDisable color="primary" [disableState]="true">Click me!</app-button> <app-toggle appColor appDisable color="secondary" [disableState]="false"></app-toggle> <app-spinner appColor color="primary"></app-spinner> |
Z założenia, dyrektywy te zawsze będą używane w zakresie tych komponentów. Wyobraź sobie, że istnieje wiele instancji komponentów toggle:
1 2 3 4 5 6 |
<app-toggle appColor appDisable [disableState]="false" color="primary"></app-toggle> <app-toggle appColor appDisable [disableState]="true" color="secondary"></app-toggle> <app-toggle appColor appDisable [disableState]="false" color="secondary"></app-toggle> <app-toggle appColor appDisable [disableState]="false" color="secondary"></app-toggle> <app-toggle appColor appDisable [disableState]="true" color="secondary"></app-toggle> <app-toggle appColor appDisable [disableState]="false" color="secondary"></app-toggle> |
Występuje tutaj dosyć spora redundancja dyrektyw appColor oraz appDisable, co nie jest zbyt czytelne. W tym przypadku z pomocą przychodzi Directive composition API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Component({ selector: 'app-toggle', standalone: true, hostDirectives: [ { directive: DisableDirective, inputs: ['disableState: disabled'], }, { directive: ColorDirective, inputs: ['color'], }, ], template: `<label class="switch"> <input type="checkbox"/> <span class="slider"></span> </label> `, styleUrls: ['./toggle.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ToggleComponent {} |
Od 15 wersji Angulara dostępne jest nowe property komponentu o nazwie hostDirectives. Pozwala ona określić tablicę dyrektyw, które powinny zostać zaaplikowane w naszych komponentach. Można również ustawić aliasy dla Inputów i
Outputów, aby zapobiec konfliktom między dyrektywą a elementami hosta. Ważne jest, aby pamiętać, że dyrektywa używająca hostDirectives musi być standalone.
Przyjrzyjmy się teraz naszym komponentom toggle
1 2 3 4 5 6 |
<app-toggle [disabled]="false" color="secondary"></app-toggle> <app-toggle [disabled]="true" color="primary"></app-toggle> <app-toggle [disabled]="false" color="primary"></app-toggle> <app-toggle [disabled]="false" color="primary"></app-toggle> <app-toggle [disabled]="true" color="primary"></app-toggle> <app-toggle [disabled]="false" color="primary"></app-toggle> |
Teraz jest to dużo czytelniejsze!
W naszym przypadku skomponowaliśmy dyrektywy z komponentami, ale możliwe jest również komponowanie dyrektyw z innymi dyrektywami.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Directive({ selector: '[appToggleTheme]', standalone: true, hostDirectives: [ { directive: DisableDirective, inputs: ['disableState: disabled'], }, { directive: ColorDirective, inputs: ['color'], }, ], }) export class ToggleThemeDirective implements AfterViewInit { @Input() isRounded = true; constructor(private elRef: ElementRef) {} ngAfterViewInit() { const sliderRef = this.elRef.nativeElement.querySelector('.slider'); if (sliderRef && this.isRounded) { sliderRef.classList.add('slider-round'); } } } |
Tutaj mamy dyrektywę ToggleThemeDirective, która składa się z ColorDirective, DisableDirective oraz posiada również swój własny skrawek funkcjonalności – zmianę kształtu toggle’a. Teraz musimy tylko zaaplikować naszą dyrektywę ToggleThemeDirective w następujący sposób:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Component({ selector: 'app-toggle', standalone: true, hostDirectives: [{ directive: ToggleThemeDirective, inputs: ['isRounded'] }], template: `<label class="switch"> <input type="checkbox" /> <span class="slider round"></span> </label> `, styleUrls: ['./toggle.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ToggleComponent {} |
Kolejność wykonywania dyrektyw
Host Directives mają taki sam cykl życia jak inne komponenty i dyrektywy w templatce. Ważne jest, aby pamiętać, że wywołanie konstruktora, lifecycle hooków i bindingów dyrektyw hosta zawsze występują przed dyrektywą lub komponentem, do którego się odnoszą.
Przeanalizujmy kolejność wykonywania ToggleComponent aplikującego ToggleThemeDirective. Dyrektywa ta, jak pokazano wcześniej, składa się z dyrektyw Disable, Color wraz z dodatkową funkcjonalnością. Załóżmy, że dyrektywy te mają zaimplementowany jedynie lifecycle hook ngOnInit.
Kolejność wykonywania wyglądałaby następująco:
- Wywołanie konstruktora DisableDirective
- Wywołanie konstruktora ColorDirective
- Wywołanie konstruktora ToggleThemeDirective
- Dyrektywa Disable ngOnInit
- Dyrektywa Color ngOnInit
- Dyrektywa ToggleTheme ngOnInit
- Zaaplikowanie bindingów DisableDirective
- Zaaplikowanie bindingów ColorDirective
- Zaaplikowanie bindingów ToggleThemeDirective
Wydajność
Owa funkcjonalność jest naprawdę wygodna, chociaż czasami, gdy jest używana w nieprzemyślany sposób, może prowadzić do problemów związanych z alokacją pamięci. Dla przykładu, przyjrzyjmy się komponentowi Toggle. Składa się on z dwóch dyrektyw i po wyrenderowaniu utworzone zostaną 3 obiekty (w tym obiekt toggle). Dla niewielkiej ilości komponentów toggle nie będzie to miało większego znaczenia, ale wyobraźmy sobie, że mamy ich setki (np. wewnątrz dużej tabeli), wtedy różnica może być zauważalna.
Podsumowanie
Directive composition API
to potężna, długo oczekiwana funkcjonalność, którą można wykorzystać do poprawy jakości i czytelności kodu. Trzeba jednak pamiętać, że korzystanie z niej powinno być świadomą decyzją ze względu na możliwe problemy z wydajnością, gdy jest używana w nieodpowiedniej sytuacji.
Dzięki za uwagę!
Dodaj komentarz