Korzyści z używania właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.
Mam na imię Dave i jestem starszym programistą front-endu w Nordhealth. Zajmuję się projektowaniem i opracowywaniem naszego systemu projektowania Nord, który obejmuje tworzenie komponentów internetowych do naszej biblioteki komponentów. Chcę się podzielić tym, jak rozwiązaliśmy problemy związane ze stylizacją komponentów internetowych za pomocą niestandardowych właściwości CSS, oraz innymi korzyściami wynikającymi z używania właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.
Jak tworzymy komponenty sieciowe
Do tworzenia naszych komponentów internetowych używamy biblioteki Lit, która zawiera wiele gotowych elementów kodu, takich jak stan, style o ograniczonym zakresie, szablony i inne. Lit jest nie tylko lekki, ale też oparty na natywnych interfejsach JavaScript API, co oznacza, że możemy dostarczyć niewielki pakiet kodu, który wykorzystuje funkcje, które przeglądarka już ma.
import {html, css, LitElement} from 'lit';
export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;
  static properties = {
    name: {type: String},
  };
  constructor() {
    super();
    this.name = 'there';
  }
  render() {
    return html`Hey 
${this.name}, welcome to Web Components!`;
  }
}
customElements.define('simple-greeting', SimpleGreeting);
Najbardziej atrakcyjne w komponentach internetowych jest to, że działają one z niemal każdą istniejącą platformą JavaScript, a nawet bez niej. Gdy główny pakiet JavaScript zostanie przywołany na stronie, używanie komponentu internetowego będzie bardzo podobne do używania natywnego elementu HTML. Jedynym prawdziwym znakiem, że nie jest to natywny element HTML, jest spójny łącznik w tagach, który jest standardem informującym przeglądarkę, że jest to komponent internetowy.
Hermetyzacja stylów w shadow DOM
Podobnie jak natywne elementy HTML mają Shadow DOM, tak samo komponenty internetowe. Shadow DOM to ukryte drzewo węzłów w elemencie. Najlepiej to zobrazować, otwierając inspektora sieci i włączając opcję „Pokaż drzewo Shadow DOM”. Gdy to zrobisz, spróbuj przyjrzeć się natywnemu elementowi wejściowemu w inspektorze – będziesz mieć teraz możliwość otwarcia tego elementu i wyświetlenia wszystkich elementów w nim zawartych. Możesz nawet spróbować to zrobić z jednym z naszych komponentów internetowych – sprawdź nasz niestandardowy komponent wejściowy, aby zobaczyć jego model Shadow DOM.
Jedną z zalet (lub wad, w zależności od perspektywy) modelu Shadow DOM jest hermetyzacja stylów. Jeśli napiszesz kod CSS w komponencie internetowym, style nie będą mogły wyciekać i wpływać na stronę główną ani inne elementy. Będą one całkowicie zawarte w komponencie. Dodatkowo kod CSS napisany dla strony głównej lub nadrzędnego komponentu internetowego nie może przenikać do komponentu internetowego.
To hermetyzowanie stylów jest zaletą naszej biblioteki komponentów. Daje nam to większą gwarancję, że gdy ktoś użyje jednego z naszych komponentów, będzie on wyglądał zgodnie z naszymi założeniami, niezależnie od stylów zastosowanych na stronie nadrzędnej. Aby mieć pewność, dodajemy all: unset; do głównego elementu wszystkich naszych komponentów internetowych.
:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Co jednak, jeśli ktoś używający Twojego komponentu internetowego ma uzasadniony powód, aby zmienić niektóre style? Może w danym kontekście linia tekstu wymaga większego kontrastu lub obramowanie powinno być grubsze? Jeśli do komponentu nie można zastosować żadnych stylów, jak odblokować te opcje stylizacji?
Właśnie dlatego powstały niestandardowe właściwości CSS.
Niestandardowe właściwości CSS
Właściwości niestandardowe to trafna nazwa – są to właściwości CSS, które możesz w całości nazwać samodzielnie i zastosować do nich dowolną wartość. Jedynym wymaganiem jest dodanie przed nimi dwóch łączników. Po zadeklarowaniu właściwości niestandardowej jej wartość można wykorzystać w CSS za pomocą funkcji var().
:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}
.n-color-accent-text {
  color: var(--n-color-accent);
}
W przypadku dziedziczenia wszystkie właściwości niestandardowe są dziedziczone, co jest typowe dla zwykłych właściwości i wartości CSS. Każda właściwość niestandardowa zastosowana do elementu nadrzędnego lub samego elementu może być używana jako wartość w innych właściwościach. W przypadku naszych tokenów projektowych intensywnie korzystamy z niestandardowych właściwości, stosując je do elementu głównego za pomocą naszego frameworka CSS. Oznacza to, że wszystkie elementy na stronie mogą używać tych wartości tokenów, niezależnie od tego, czy jest to komponent internetowy, pomocnicza klasa CSS czy programista, który chce pobrać wartość z naszej listy tokenów.
Możliwość dziedziczenia właściwości niestandardowych za pomocą funkcji var() pozwala nam przenikać przez Shadow DOM naszych komponentów internetowych i zapewnia programistom większą kontrolę nad stylem naszych komponentów.
Właściwości niestandardowe w komponencie internetowym Nord
Podczas tworzenia komponentu do naszego systemu projektowania starannie podchodzimy do jego kodu CSS – chcemy, aby był prosty, ale łatwy w utrzymaniu. Zdefiniowane przez nas tokeny projektu są zdefiniowane jako właściwości niestandardowe w naszej głównej platformie CSS w elemencie głównym.
:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Wartości tych tokenów są następnie używane w naszych komponentach. W niektórych przypadkach zastosujemy wartość bezpośrednio do właściwości CSS, ale w innych zdefiniujemy nową kontekstową właściwość niestandardową i zastosujemy do niej wartość.
:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}
.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
Abstrahujemy też niektóre wartości, które są specyficzne dla komponentu, ale nie występują w naszych tokenach, i przekształcamy je w kontekstową właściwość niestandardową. Właściwości niestandardowe, które są kontekstowe w stosunku do komponentu, zapewniają nam 2 kluczowe korzyści. Po pierwsze, możemy używać bardziej „suchego” kodu CSS, ponieważ tę wartość można zastosować do wielu właściwości w komponencie.
.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}
.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
Po drugie, sprawia, że zmiany stanu komponentu i wersji są bardzo przejrzyste – wystarczy zmienić tylko właściwość niestandardową, aby zaktualizować wszystkie te właściwości, np. podczas stylizowania stanu najechanej lub aktywnej pozycji albo w tym przypadku wersji.
:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Największą zaletą jest jednak to, że gdy definiujemy te kontekstowe właściwości niestandardowe w komponencie, tworzymy rodzaj niestandardowego interfejsu API CSS dla każdego z naszych komponentów, z którego może korzystać użytkownik tego komponentu.
<nord-tab-group label="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup
style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
W przykładzie powyżej widać jeden z naszych komponentów internetowych, w którym za pomocą selektora zmieniono kontekstową właściwość niestandardową. W rezultacie otrzymujemy komponent, który zapewnia użytkownikowi wystarczającą elastyczność w zakresie stylizacji, a jednocześnie zachowuje większość rzeczywistych stylów. Dodatkowo my, jako deweloperzy komponentów, mamy możliwość przechwytywania stylów zastosowanych przez użytkownika. Jeśli będziemy chcieli dostosować lub rozszerzyć jedną z tych właściwości, możemy to zrobić bez konieczności wprowadzania przez użytkownika zmian w kodzie.
Uważamy, że to podejście jest niezwykle skuteczne nie tylko dla nas jako twórców komponentów systemu projektowania, ale także dla naszego zespołu programistów, gdy używają tych komponentów w naszych usługach.
Dalsze wykorzystanie właściwości niestandardowych
W momencie pisania tego artykułu nie ujawniamy tych kontekstowych właściwości niestandardowych w naszej dokumentacji, ale planujemy to zrobić, aby nasz zespół deweloperów mógł je zrozumieć i wykorzystać. Nasze komponenty są pakowane w npm z plikiem manifestu, który zawiera wszystkie informacje o nich. Następnie używamy pliku manifestu jako danych podczas wdrażania naszej witryny z dokumentacją, co robimy za pomocą Eleventy i jego funkcji danych globalnych. Planujemy uwzględnić te kontekstowe właściwości niestandardowe w tym pliku danych manifestu.
Chcemy też ulepszyć sposób, w jaki te kontekstowe właściwości niestandardowe dziedziczą wartości. Obecnie, jeśli chcesz na przykład dostosować kolor 2 komponentów separatora, musisz kierować reklamy na oba te komponenty za pomocą selektorów lub zastosować właściwość niestandardową bezpośrednio do elementu z atrybutem stylu. Może się to wydawać w porządku, ale byłoby bardziej przydatne, gdyby deweloper mógł zdefiniować te style w elemencie zawierającym lub nawet na poziomie głównym.
<nord-divider></nord-divider>
<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>
<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }
  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Wartość właściwości niestandardowej musisz ustawić bezpośrednio w komponencie, ponieważ definiujemy ją w tym samym elemencie za pomocą selektora hosta komponentu. Globalne tokeny projektu, których używamy bezpośrednio w komponencie, przechodzą bez zmian i nie są objęte tym problemem. Można je nawet przechwytywać w elementach nadrzędnych. Jak możemy połączyć zalety obu tych rozwiązań?
Prywatne i publiczne właściwości niestandardowe
Prywatne właściwości niestandardowe to rozwiązanie opracowane przez Leę Verou. Jest to kontekstowa „prywatna” właściwość niestandardowa w samym komponencie, ale ustawiona jako „publiczna” właściwość niestandardowa z wartością domyślną.
:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}
.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
Zdefiniowanie w ten sposób kontekstowych właściwości niestandardowych oznacza, że nadal możemy robić wszystko, co robiliśmy wcześniej, np. dziedziczyć globalne wartości tokenów i ponownie wykorzystywać wartości w kodzie komponentu. Komponent będzie też płynnie dziedziczyć nowe definicje tej właściwości w sobie lub w dowolnym elemencie nadrzędnym.
<nord-divider></nord-divider>
<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>
<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }
  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Można argumentować, że ta metoda nie jest w pełni „prywatna”, ale uważamy, że jest to dość eleganckie rozwiązanie problemu, który nas niepokoił. Gdy będziemy mieć taką możliwość, zajmiemy się tym w naszych komponentach, aby nasz zespół programistów miał większą kontrolę nad ich użyciem, a jednocześnie korzystał z obowiązujących zasad.
Mamy nadzieję, że te informacje o tym, jak używamy komponentów internetowych z niestandardowymi właściwościami CSS, okazały się przydatne. Podziel się z nami swoją opinią. Jeśli zdecydujesz się wykorzystać którąś z tych metod w swojej pracy, możesz znaleźć mnie na Twitterze @DavidDarnes. Możesz też znaleźć Nordhealth na Twitterze: @NordhealthHQ. Obserwuj też resztę mojego zespołu, który ciężko pracował nad stworzeniem tego systemu projektowania i wdrożeniem funkcji wymienionych w tym artykule: @Viljamis, @WickyNilliams i @eric_habich.
Baner powitalny autorstwa Dana Cristiana Pădureța