Wróć do strony głównej
Angular

Angular Styles Masterclass

Jako frontend deweloperzy jesteśmy odpowiedzialni za różnego rodzaju funkcjonalności w aplikacjach internetowych. Moim zdaniem drugą najważniejszą rzeczą zaraz po logice biznesowej jest zarządzanie stylami w aplikacji. To właśnie dzięki wielu design systemom, nasze aplikacje mogą wyglądać tak, by zachęcić użytkowników do interakcji z produktem.

Zajrzymy jakie ficzery oferuje nam Angular i SCSS w kontekście CSS’a. Po przeczytaniu tego artykułu będziesz wiedział czym jest enkapsulacja stylów, jakie selektory SCSS możemy użyć w Angularze oraz czym jest design system.

Czym są Style Globalne?

Na początek zacznijmy od czegoś prostego – stylów globalnych. Style globalne są to style, które aplikowane są do elementów w obrębie całej aplikacji, bez znaczenia na położenie pliku HTML.

Dla przykładu: jeżeli w domyślnym pliku z globalnymi stylami styles.scss zadeklarujemy klasę .red-highlight, to będziemy mogli się do niej odnieść w każdym komponencie wewnątrz naszej aplikacji tak aby ostylować ten element (wyjątkiem są komponenty używające enkapsulacji ShadowDom). 

styles.scss

app.component.html

Jako rezultat powyższego kodu w przeglądarce zobaczymy tekst Example Box z czerwonym tłem. Jak możesz zauważyć używanie stylów globalnych jest banalnie proste.

W tych stylach globalnych powinniśmy zrobić takie rzeczy jak:

  • zadeklarować wszelkiego rodzaju utility klasy (typografia, marginesy, padding)
  • zaaplikować CSS reset, tak aby zatrzeć różnice w wyświetlaniu elementów przez różne przeglądarki. W rezultacie zaaplikowania CSS resetu margin-bottom dla paragrafów będą jednakowe zarówno w safari jak i w chrome.
  • zainicjalizować zmienne CSS/SCSS
  • zainicjalizować theme z bibliotek ui (@angular/material)
  • nadpisać/modyfikować style z bibliotek ui (np @angular/material)

Konfiguracja Stylów Globalnych

Skoro już wiemy czym są style globalne, możemy przejść do ich konfiguracji. Ta różni się lekko w zależności od tego czy używamy biblioteki @nrwl/nx w projekcie. Style globalne skonfigurujemy w pliku:

  • angular.json (Jeżeli nie używamy Nx’a)
  • project.json (Jeżeli używamy Nx’a)

W przypadku pliku angular.json ścieżki do arkusza stylów globalnych zadeklarujemy w json’ie pod ścieżką:

projects > (nazwa projektu) > architect > (konfiguracja) > options > styles

angular.json

W przypadku pliku project.json zadeklarujemy ścieżki pod kluczem:

targets > (konfiguracja) > options > styles

project.json

Domyślnie każda angularowa aplikacja ma zaimportowany tylko jeden plik ze stylami globalnymi – styles.scss znajdujący się w folderze src. W liście stylów powinniśmy zaimportować style z zewnętrznych bibliotek UI np takich jak @angular/material czy bootstrap.

Dodanie Stylu Globalnego (Bootstrap)

Do przykładu konfiguracji stylów globalnych posłużę się biblioteką bootstrap. Biblioteka ta dostarcza nam utility klasy oraz gotowe już komponenty, które znacznie przyśpieszają proces budowania UI dla aplikacji. Możemy ją dodać do naszego projektu za pomocą komendy:

Po zainstalowaniu biblioteki zarejestrujemy style globalne bootstrapa w pliku angular.json lub project.json w zależności od naszego stacku technologicznego.

Każda ingerencja w konfigurację styli globalnych wymaga od nas zrestartowania skryptu ng serve w celu zaaplikowania zmian. Po ponownym uruchomieniu powinniśmy być w stanie używać klas i komponentów pochodzących z bootstrapa. (Przy instalacji bootstrapa należy również zaimportować do projektu plik js)

Jak mogłeś zauważyć konfiguracja stylów globalnych jest banalnie prosta! Przejdźmy do czegoś troszeczkę bardziej skomplikowanego – importowania stylów do komponentu.

Stylowanie Komponentów

W większości przypadków każdy komponent w Angularze posiada osobny “lokalny” plik ze stylami. Dzięki takiemu podejściu aplikacje zachowują modularność i mają przejrzystą strukturę.

Importowanie stylów w komponencie

Aby zaimportować jeden plik lub kilka plików ze stylami możemy użyć następujących propsów wewnątrz dekoratora @Component:

  • styleUrls – przyjmuje listę względnych ścieżek do plików ze stylami.
  • styleUrl – przyjmuje ścieżkę do pliku jako w string. Warto zaznaczyć, że props jest dostępny dopiero od angulara 17.0.0-next.4

W przypadkach gdzie chcemy podać CSS bezpośrednio w pliku z komponentem to możemy użyć propsa styles wewnątrz dekoratora @Component. Props ten przyjmuje listę stylów. Warto jednak dodać, że od Angulara 17.0.0-next.4 wartość propsa może również być stringiem.

Od siebie chciałbym dodać, że w świecie Angulara pisanie stylów w pliku z komponentem jest rzadką praktyką. Po to rozwiązanie powinniśmy sięgać tylko wtedy, gdy kod CSS naszego komponentu jest bardzo krótki.

Różne Rodzaje Enkapsulacji Stylów

Skoro już wiemy jak zaimportować style do komponentu, możemy przejść do typów enkapsulacji stylów. Każdy typ enkapsulacji działa zupełnie inaczej – warto znać różnice pomiędzy nimi, aby uniknąć przykrych niespodzianek w przyszłości.

Na chwilę obecną możemy skorzystać z następujących enkapsulacji:

  • Emulated (Domyślna)
  • ShadowDOM
  • None

Enkapsulacja Emulated – Lokalne Style (domyślna)

Ta enkapsulacja sprawia, że zaimportowane style będą wpływać tylko na elementy wewnątrz templatki naszego komponentu. Dzięki takiemu zachowaniu styli możemy być pewni, że style komponentu A nie nadpiszą styli komponentu B. Ta cecha sprawia, że aplikacje oparte o tą enkapsulację styli są niezawodne – wraz z dodawaniem kolejnych ficzerów do aplikacji całkowicie unikamy błędów wizualnych spowodowanych nadpisywaniem się klas.

Enkapsulację Emulated możemy zadeklarować w property encapsulation dekoratora @Component. Jest to jednak opcjonalne, ponieważ Angular wykorzystuje ten typ enkapsulacji domyślnie dla każdego komponentu.

Enkapsulacja ShadowDOM

Enkapsulacja ShadowDOM – W przypadku wykorzystania tej enkapsulacji komponent zostanie stworzony w specjalnym Shadow Roocie. Komponenty wykorzystujące ten typ enkapsulacji są izolowane od głównego DOM’u przez co NIE BĘDĄ mogły używać stylów globalnych.

Przy użyciu enkapsulacji ShadowDOM warto mieć pod uwagą kompatybilność przeglądarek (link).

Shadow root’y w drzewie DOM są zwrappowane w element #shadow-root.

Powyższy screen przedstawia DOM po włączeniu enkapsulacji ShadowDOM w komponencie app-root.

Enkapsulacja None

Ostatnim typem enkapsulacji jest Enkapsulacja None, która sprawia, że wszystkie style w naszym komponencie będą globalne.

Wykorzystywanie enkapsulacji none jest bardzo ryzykowne, ponieważ wraz z dodawaniem nowych ficzerów możemy doświadczyć wizualnej regresji – nieoczekiwanej zmiany wyglądu komponentu.

W przypadku enkapsulacji None wizualnej regresji doświadczymy, gdy zadeklarujemy 2 klasy o tej samej nazwie. W miarę możliwości ograniczyłbym użycie Enkapsulacji None – znacznie bardziej bezpiecznym rozwiązaniem jest korzystanie z Enkapsulacji Emulated.

W przypadkach gdzie decydujemy się na użycie wyłączonej enkapsulacji warto wrapować style w unikalną klasę, która nie powtórzy się w aplikacji. Dobrym pomysłem jest użycie nazwy komponentu jako nazwy dla klasy wrapującej – dzięki temu szansa na przypadkowe powtórzenie klasy jest mała. Dla przykładu:

File: color-picker.component.ts

File: styles.scss (globalne style)

Selektory w komponencie

W naszych aplikacjach możemy używać specjalnych selektorów w stylach, które pozwalają nam targetować tylko wybrane elementy.

Selektor :host

Ten selektor pozwala nam na stylowanie tagu naszego komponentu.

Dla powyższego pliku CSS zaimportowanego do komponentu z użyciem selektora app-card powyższy kod SCSS zostanie skompilowany do poniższego kodu CSS:

Domyślnie każdy Angularowy komponent jest inline więc użycie tego selektora do ustawienia display:block jest stosunkowo popularne.

Warto zauważyć, że selektor :host ma swoje ograniczenie oraz działa tylko wtedy, kiedy nasz komponent używa enkapsulacji Emulated lub ShadowDOM. W przypadku enkapsulacji None będziemy musieli lekko zmodyfikować powyższy kod aby zaaplikować style do naszego elementu:

Selektor :host-context

Ten selektor pozwala nam na stylowanie elementu hostującego komponent na podstawie klasy w elemencie rodzica. Możemy użyć tej funkcjonalności np. do stylowania naszego komponentu w zależności od klasy theme w body.

Dla przykładu w poniższy styl zostanie zaaplikowany do klasy my-button wewnątrz naszego komponentu tylko wtedy, kiedy jeden z przodków (w naszym przypadku element body) będzie miał klasę dark-theme

Selektor ::ng-deep

Za pomocą selektora ::ng-deep mogliśmy stylować elementy dzieci naszego komponentu. Warto zauważyć, że ten selector jest oznaczony jako deprecated (dokumentacja ng-deep). Zdania na temat tego czy powinniśmy używać ng-deep są podzielone – znajdziemy zarówno wielu zwolenników jak i przeciwników tego selektora.

Design sytem – globalne zmienne SCSS

Bardzo dobrą praktyką w tworzeniu aplikacji internetowych jest korzystanie z design systemu – zestawu wspólnych wartości dla aplikacji. Wartościami takimi mogą być chociażby jednostki odległości, kolory czy typografia.

W tej sekcji artykułu zaimplementujemy prosty design systemu z pomocą zmiennych SCSS, które będą przechowywać kolory, jednostki długości, a nawet breakpointy!

Aby móc skorzystać z globalnych zmiennych i mixinów musimy odpowiednio skonfigurować aplikację w angular.json lub project.json. Pod sekcją w której dodawaliśmy globalne style dodajemy 2 nowe sekcje: stylePreprocessorOptions oraz includePaths. Od teraz po restarcie CLI Angular pozwoli nam  na import zmiennych i utili w stylach lokalnych za pomocą ścieżki zaczynającej się w katalogu “src”

Zacznijmy od stworzenia struktury plików:

  1. Stwórzmy folder styles oraz zagnieżdżony w nim folder utils
  2. Wewnątrz folderu utils stwórzmy 3 pliki SCSS – _breakpoints, _colors oraz _spacing
  3. Dodajmy plik index.scss wewnątrz folderu utils

Nasza struktura powinna wyglądać w następujący sposób w drzewie folderów:

Po tym jak stworzyliśmy pliki możemy przejść do napisania naszego design systemu. Zacznijmy od zdefiniowania naszej palety kolorów w pliku _colors.scss. Aby ułatwić sobie proces ich dobierania możemy posłużyć się generatorami palet online czy stworzonych już palet kolorów (tailwind, material design). Przy doborze kolorów najlepiej trzymać się podejścia gdzie komponujemy nasze UI z:

  • Koloru przewodzącego (od ang. primary color)
  • Koloru akcentującego (od ang. accent color)
  • Koloru ostrzegającego (żółty/pomarańczowy) (od ang. warn color)
  • Koloru do oznaczenia błędów (czerwony) (od ang. error color)

Podczas tworzenia elementów UI powinniśmy również zaaplikować zasadę 60/30/10. Zasada ta polega na dobieraniu kolorów interfejsu w taki sposób, aby ui składało się w:

  • 60% z podstawowego koloru (najczęściej jest to kolor naturalny tj. biały/szary/czarny)
  • 30% z koloru przewodzącego
  • 10% z koloru akcentującego

Stosując tą zasadę nasze UI będzie bardzo przejrzyste, a wszystkie elementy interaktywne (np. buttony) z kolorem do akceptacji opcji będą się z łatwością wyróżniać i wręcz prosić o kliknięcie!

Przykładowy plik _colors.scss może wyglądać tak:

File: _colors.scss

Nasza aplikacja poza jednolitym systemem kolorów powinna również posiadać jednakowy spacing, który będzie odpowiednio oddzielał elementy UI. Generalna zasada jest taka, by używać zawsze spacingu 8/12px w obrębie jednego komponentu UI oraz większego spacingu w celu oddzielenia komponentów od siebie.

Przykładowy plik ze space’ingiem:

File: _spacing.scss

Ostatnią rzeczą, którą zadeklarujemy w naszym design systemie będą breakpointy. Przy pisaniu aplikacji powinniśmy skupić się na 3 viewportach:

  • smartfony
  • tablety / notebooki z małym ekranem
  • monitory komputerowe / telewizory

Warto stworzyć sobie kilka zmiennych przechowujących wyżej wymienione breakpointy. Przydatne okażą się też mixiny, które z łatwością pozwolą nam pisać responsywne UI.

Przykładowy plik z breakpointami może wyglądać w taki sposób:

File: _breakpoints.scss

To by było na tyle z naszego małego design systemu.

Nadszedł czas na eksport zmiennych z wszystkich plików. Możemy to zrobić poprzez użycie @forward w pliku index.scss. Będziemy jeszcze musieli również użyć @forward w globalnym pliku ze stylami.

File: index.scss

Aby użyć naszego design systemu wewnątrz komponentu skorzystamy z @use, w którym deklarujemy ścieżkę do folderu w którym znajduje się plik index.scss.

File: app.component.scss

Warto pamiętać, że tworzenie własnego design systemu wymaga od nas pisania dużej ilości boiler-plate kodu. Na rynku są już dostępne narzędzia takie jak np: Tailwind CSS, które implementują własny design system i pozwalają nam na dostosowanie go pod nasze potrzeby.

Dyrektywy

Dyrektywy to angularowa struktura, za pomocą której możemy pisać reużywalny kod odpowiedzialny za manipulowanie zachowaniami oraz wyglądem elementu w naszej aplikacji.

W tej części artykułu stworzymy dyrektywy, które pozwolą nam na zaaplikowanie tęczowego tła dla naszego elementu. Spojrzymy na 3 różne sposoby, pozwalające nam na manipulowanie stylami elementu. Nasze dyrektywy zastosują takie technologie jak:

  • Wrapper natywnego elementu HTML ElementRef
  • dekorator @HostBinding
  • property “host” dyrektywy/komponentu

Zacznijmy więc! Stwórzmy za pomocą CLI 3 dyrektywy

Po stworzeniu dyrektyw powinniśmy mieć następującą strukturę plików:

Zanim przejdziemy do implementacji logiki dyrektyw stwórzmy mały setup dla naszego kodu. Zacznijmy od zadeklarowania globalnego stylu nadającego naszemu elementowi tęczowe tło w pliku styles.scss.

Następnie importujemy nasze dyrektywy w app.component.ts

Oraz finalnie dodajmy 3 divy w głównym widoku naszej aplikacji, tak aby na bieżąco widzieć jak dyrektywy wpływają na elementy HTML.

File: app.component.html

File: app.component.scss

Okej, to by było na tyle z setupu naszej aplikacji. Przejdźmy do implementacji dyrektyw – zaczniemy od tej używającej ElementRef.

Element Ref i Natywny Element

Podzielmy sobie logikę tego komponentu na 3 sekcje:

Inicjalizacja:

  • Najpierw wstrzykujemy ElementRef i przypisujemy go do zmiennej _elementRef

Logika Stylów:

  • Tworzymy @Input duration, który przy bindowaniu wartości ustawi czas trwania animacji
  • Tworzymy @Input hideBackground, który w zależności od wartości usunie/doda klasę “rainbow-background” do naszego elementu

Domyślne Wartości

  • Dodajemy do klasy interfejs OnInit oraz implementujemy metodę ngOnInit, w której podajemy domyślne wartości inputów w naszym komponencie.

Po uruchomieniu kodu w przeglądarce powinniśmy zobaczyć, że nasz element uzyskał tęczowe tło!

W mojej opinii używanie ElementRef jest bardzo toporne – przy stylowaniu elementów trzeba wykonywać wiele rzeczy manualnie. Wyżej wymieniona klasa przyda nam się, gdy chcemy napisać logikę do bardziej zaawansowanych ficzerów np. Elementu Badge. Na całe szczęście w większości przypadków możemy skorzystać z prostszych ficzerów angulara do stylowania elementów – np dekoratora @HostBinding.

Dekorator @HostBinding

Trochę lepszym sposobem na stylowanie elementów z użyciem dyrektyw jest wykorzystanie dekoratora @HostBinding. Z jego pomocą Angular automatycznie przypisze atrybuty do naszego elementu zgodnie z wartością zmiennej/gettera.

Spójrzmy na powyższy przykład:

Inicjalizacja:

  • Deklarujemy dwa inputy – duration i hideBackground.

Logika Stylów

  • Bindujemy styl animationDuration do elementu za pomocą dekoratora @HostBinding. Styl animation-duration na naszym elemencie będzie miał taką samą wartość jak getter animationDuration (domyślnie “5s”).
  • Bindujemy klasę rainbow-backgrond, która będzie widniała na elemencie tylko wtedy kiedy @Input hideBackground naszej dyrektywy będzie równy false.

Jak z pewnością widzisz, dekorator @HostBinding pozwala nam na znaczne skrócenie logiki  stylowania. Pytanie jest, czy możemy zrobić coś jeszcze lepiej?

Host Property Dyrektywy/Komponentu

Property host wewnątrz dekoratora naszej dyrektywy pozwala nam na bardzo czytelne bindowanie atrybutów do naszego komponentu. W powyższym przykładzie:

Inicjalizacja:

  • Deklarujemy dwa inputy – duration i hideBackground.

Logika Stylów:

  • Bindujemy klasę rainbow-background do elementu, kiedy hideBackground będzie równe false
  • Bindujemy styl animationDuration do wartości zmiennej duration oraz dodajemy “s”.

Podsumowanie

Od teraz style w Angularze nie powinny skrywać przed tobą tajemnic. W mojej opinii Angular dostarcza nam dużo ficzerów w kontekście CSS’a. Dzięki selektorom, scope’owanych stylach czy globalnych zmiennych SCSS jesteśmy w stanie budować interfejsy użytkownika nastawione przede wszystkim na spójność.

O autorze

Dawid Kostka

Angular Developer w House of Angular. Software development dla mnie to coś więcej niż tylko klepanie kodu. Kocham eksplorować i bawić się wysokiej jakości kodem. Uwielbiam adrenalinę i pozytywne nastawienie. W wolnych chwilach jeżdzę na rolkach agresywnych.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

4 komentarzy

    • Dzięki za komentarz Paweł. Według mnie do deprykacji przyczyniło się kilka rzeczy:

      – na przestrzeni ostatnich lat zwiększyła się możliwość manipulacji CSS’em w bibliotekach z zewnętrznymi komponentami tj. angular material. (a to własnie w libkach najczęściej używaliśmy ::ng-deep).
      Przykładem może być chociażby materialowy dialog

      – powstawanie dziwnych bugów wizualnych. Użycie ::ng-deep w lazy-loaded modułach permanentnie aplikuje style dla całej aplikacji po załadowaniu modułu. Może to powodować wizualną regresję, ciężką do zdebugowania.
      Zobacz np ten przykład (dashboard -> profile -> dashboard)

      Osobiście odbieram deprykację jako swojego rodzaju ostrzeżenie dla nieświadomego developera – to co próbujesz zrobić może być już niestosowne i prawdopodobnie istnieje lepszy sposób by osiągnąć zamierzony cel.

Leave a Reply

Your email address will not be published. Required fields are marked *