Jak Nordhealth korzysta z właściwości niestandardowych w komponentach sieciowych

Zalety korzystania z właściwości niestandardowych w systemach projektowania i bibliotekach komponentów.

David Darnes
David Darnes

Nazywam się Dave i pracuję jako starszy programista front-endu w firmie Nordhealth. Pracuję nad projektem i rozwojem naszego systemu projektowego Nord, który obejmuje tworzenie komponentów internetowych do naszej biblioteki komponentów. Chcę opowiedzieć, jak rozwiązaliśmy problemy związane ze stylizacją komponentów internetowych, używając właściwości niestandardowych CSS, oraz o niektórych innych zaletach korzystania z 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 fragmentów kodu, takich jak stan, ograniczone style, szablony itp. Biblioteka Lit jest nie tylko lekka, ale też oparta na natywnych interfejsach API JavaScriptu, co oznacza, że możemy dostarczyć prosty pakiet kodu, który korzysta z funkcji już dostępnych w przeglądarce.


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);
Element webowy napisany za pomocą Lit.

Największą zaletą komponentów internetowych jest jednak to, że działają one z prawie każdym istniejącym frameworkiem JavaScriptu, a nawet bez frameworka. Gdy na stronie jest już wskazany główny pakiet JavaScriptu, używanie komponentu internetowego jest bardzo podobne do korzystania z elementu HTML. Jedynym prawdziwym znakiem, że nie jest to natywny element HTML, jest konsekwentne stosowanie myślnika w tagach, co jest standardem oznaczającym dla przeglądarki, że jest to komponent internetowy.

Hermetyzacja stylu w modelu shadow DOM

Podobnie jak natywne elementy HTML mają model Shadow DOM, tak samo jest w przypadku komponentów internetowych. Shadow DOM to ukryte drzewo węzłów w elemencie. Najlepszym sposobem na wizualizację tego procesu jest otwarcie narzędzia do inspekcji witryny i włączenie opcji „Pokaż drzewo Shadow DOM”. Następnie w inspektorze sprawdź element domyślnego wejścia. Teraz możesz otworzyć to wejście i zobaczyć wszystkie jego elementy. Możesz to nawet wypróbować na jednym z naszych komponentów internetowych. Spróbuj sprawdzić nasz niestandardowy komponent pola wejściowego, aby zobaczyć jego model Shadow DOM.

Shadow DOM w Narzędziach deweloperskich.
Przykład modelu Shadow DOM w zwykłym elemencie tekstowego pola wejściowego i w komponencie internetowym Nord.

Jedną z zalet (lub wad, w zależności od punktu widzenia) modelu Shadow DOM jest hermetyzacja stylów. Jeśli napiszesz kod CSS w komponencie internetowym, te style nie będą mogły przenikać na stronę główną ani inne elementy. Będą całkowicie zawarte w komponencie. Ponadto kod CSS napisany dla strony głównej lub nadrzędnego komponentu internetowego nie może przenikać do Twojego komponentu internetowego.

Ta możliwość opakowania stylów jest zaletą naszej biblioteki komponentów. Dzięki temu mamy większą pewność, że gdy ktoś użyje jednego z naszych komponentów, będzie on wyglądał tak, jak chcieliśmy, niezależnie od stylów zastosowanych na stronie nadrzędnej. Aby jeszcze bardziej to ułatwić, dodaliśmy all: unset; do katalogu głównego (czyli „hosta”) wszystkich naszych komponentów internetowych.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Niektóre elementy szablonowego kodu komponentu są stosowane do katalogu głównego cienia lub selektora hosta.

Co jednak, jeśli ktoś, kto używa Twojego komponentu internetowego, ma uzasadniony powód, aby zmienić niektóre style? Może jakiś fragment tekstu wymaga większego kontrastu ze względu na kontekst lub może trzeba zastosować grubszą ramkę? Jeśli żadne style nie mogą zostać zastosowane do Twojego komponentu, jak możesz odblokować te opcje stylizacji?

Dlatego do akcji wkraczają niestandardowe właściwości CSS.

Właściwości niestandardowe w CSS

Właściwości niestandardowe mają bardzo trafne nazwy. Są to właściwości CSS, które możesz nazwać i przypisać do nich dowolną wartość. Wymaga to tylko dodania 2 łączników. Po zadeklarowaniu właściwości niestandardowej możesz używać jej wartości w CSS za pomocą funkcji var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Przykład z naszego frameworku CSS: token projektu jako właściwości niestandardowej używany w klasie pomocniczej.

Jeśli chodzi o dziedziczenie, wszystkie właściwości niestandardowe są dziedziczone zgodnie ze standardowym działaniem właściwości i wartości w CSS. Jako wartości innych właściwości można używać dowolnej właściwości niestandardowej zastosowanej do elementu nadrzędnego lub samego elementu. W przypadku naszych tokenów projektowych intensywnie korzystamy z właściwości niestandardowych, stosując je do elementu wyższego poziomu za pomocą naszego frameworku CSS. Oznacza to, że wszystkie elementy na stronie mogą korzystać z tych wartości tokenów, niezależnie od tego, czy są to komponenty internetowe, pomocnicze klasy CSS czy programista, który chce wybrać wartość z naszej listy tokenów.

Ta zdolność do dziedziczenia właściwości niestandardowych za pomocą funkcji var() pozwala nam przeniknąć przez DOM cieniowany naszych komponentów internetowych i umożliwia deweloperom bardziej szczegółową kontrolę nad stylami komponentów.

Właściwości niestandardowe w komponencie Nord Web

Podczas tworzenia komponentów do naszego systemu projektowania bardzo starannie podchodzimy do kodu CSS. Staramy się tworzyć kod zwięzły, ale bardzo łatwy do utrzymania. Nasze tokeny projektowe są zdefiniowane jako właściwości niestandardowe w głównym interfejsie 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);
  /* ... */
}
Właściwości niestandardowe arkusza CSS zdefiniowane w selowniku głównym.

Te wartości tokenów są następnie używane w naszych komponentach. W niektórych przypadkach zastosujemy wartość bezpośrednio w usłudze porównywania cen, ale w innych przypadkach zdefiniujemy nową kontekstową usługę spersonalizowaną 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);
  /* ... */
}
Właściwości niestandardowe zdefiniowane w katalogu głównego komponentu, a następnie używane w stylach komponentu. Używane są też usługi niestandardowe z listy tokenów projektu.

Utworzymy też abstrakcyjne wartości, które są specyficzne dla danego komponentu, ale nie występują w naszych tokenach, i przekształcimy je w kontekstualną właściwość niestandardową. Właściwości niestandardowe, które są kontekstowe dla komponentu, dają nam 2 kluczowe korzyści. Po pierwsze, oznacza to, że możemy bardziej „oszczędnie” korzystać z CSS, ponieważ wartość może być stosowana 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);
}
Właściwość niestandardowa kontekstowa „tab_group_padding” używana w wielu miejscach w kodzie komponentu.

Po drugie, ułatwia to wprowadzanie zmian w stanie komponentu i wariantach – wystarczy zmienić tylko właściwość niestandardową, aby zaktualizować wszystkie te właściwości, gdy np. stylizujesz stan najechania kursorem lub stan aktywny lub w tym przypadku wariant.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Wariant komponentu karty, w którym wypełnienie jest zmieniane za pomocą pojedynczej aktualizacji właściwości niestandardowej zamiast wielu aktualizacji.

Największą zaletą jest jednak to, że gdy definiujemy te kontekstowe właściwości niestandardowe w komponencie, tworzymy dla każdego z naszych komponentów rodzaj niestandardowego interfejsu API CSS, z którego może korzystać użytkownik tego komponentu.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Użyj na stronie komponentu grupy kart i zwiększ rozmiar właściwości niestandardowej wypełniania.

Poprzedni przykład pokazuje jeden z naszych komponentów internetowych z kontekstową usługą niestandardową, która została zmieniona za pomocą selektora. Efektem tego podejścia jest komponent, który zapewnia użytkownikowi wystarczającą elastyczność stylizacji, zachowując przy tym kontrolę nad większością rzeczywistych stylów. Dodatkowo my, jako deweloperzy komponentów, możemy przechwytywać style stosowane przez użytkownika. Jeśli chcemy dostosować lub rozszerzyć jedną z tych właściwości, możemy to zrobić bez konieczności zmiany kodu przez użytkownika.

Uważamy, że to podejście jest bardzo skuteczne nie tylko dla nas jako twórców komponentów systemu projektowania, ale też dla naszego zespołu programistów, który używa tych komponentów w naszych usługach.

Właściwości niestandardowe – dodatkowe informacje

W momencie pisania tego tekstu nie ujawniamy tych kontekstowych usług niestandardowych w dokumentacji, ale planujemy to zrobić, aby nasz szerszy zespół deweloperów mógł je zrozumieć i wykorzystać. Nasze komponenty są pakowane w npm za pomocą pliku manifestu, który zawiera wszystkie informacje o nich. Następnie, gdy publikujemy stronę dokumentacji, używamy pliku manifestu jako danych. Wykorzystujemy do tego Eleventy i jego funkcję Global Data. Planujemy uwzględnić te kontekstowe właściwości niestandardowe w tym pliku danych pliku manifestu.

Innym obszarem, który chcemy ulepszyć, jest sposób dziedziczenia wartości przez te kontekstowe usługi niestandardowe. Jeśli na przykład chcesz zmienić kolor 2 elementów oddzielnika, musisz je oba kierunkowo ustawić za pomocą selektorów lub zastosować właściwość niestandardową bezpośrednio na elemencie za pomocą atrybutu style. Może się to wydawać w porządku, ale lepiej byłoby, gdyby deweloper mógł zdefiniować te style w elemencie zawierającym lub nawet na poziomie katalogu głównego.


<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>
2 występowania naszego komponentu separatora, które wymagają 2 różnych kolorów. Jeden z nich jest zagnieżdżony w sekcji, którą możemy wykorzystać do bardziej szczegółowego selektora, ale musimy kierować reklamy na samą linię rozdzielającą.

Wartość właściwości niestandardowej musisz ustawić bezpośrednio w komponencie, ponieważ definiujemy je w tym samym elemencie za pomocą selektora hosta komponentu. Globalne tokeny projektu, których używamy bezpośrednio w komponencie, są przekazywane bez zmian i nie są objęte tym problemem. Można je nawet przechwycić w elementach nadrzędnych. Jak możemy połączyć zalety obu tych rozwiązań?

Prywatne i publiczne właściwości niestandardowe

Własne właściwości prywatne zostały opracowane przez Lea Verou. Są to kontekstowe „prywatne” właściwości niestandardowe w komponencie, ale ustawione jako „publiczne” z opcją zastępczą.



: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);
  /* ... */
}
CSS komponentu internetowego z elementem rozdzielającym z właściwościami niestandardowymi opartymi na kontekście, tak aby wewnętrzny kod CSS korzystał z właściwości niestandardowej prywatnej, która została ustawiona jako publiczna własność niestandardowa z wartością zastępczą.

Definiowanie kontekstowych usług niestandardowych w taki sposób oznacza, że nadal możemy wykonywać wszystkie czynności, które wykonywaliśmy wcześniej, np. dziedziczyć wartości globalnych tokenów i wykorzystywać wartości w kodzie komponentu. Komponent będzie też płynnie dziedziczyć nowe definicje tej usługi 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>
Znów 2 separatory, ale tym razem można zmienić ich kolor, dodając do selektora sekcji kontekstową właściwość niestandardową separatora. Podzielnik odziedziczy tę wartość, tworząc bardziej przejrzysty i elastyczny fragment kodu.

Chociaż można argumentować, że ta metoda nie jest naprawdę „prywatna”, nadal uważamy, że jest to dość eleganckie rozwiązanie problemu, który nas niepokoił. Gdy tylko będzie to możliwe, rozwiążemy ten problem w naszych komponentach, aby nasz zespół programistów miał większą kontrolę nad ich używaniem, a jednocześnie nadal korzystał z dostępnych zabezpieczeń.

Mam nadzieję, że te informacje o tym, jak używamy komponentów internetowych z właściwościami niestandardowymi w CSS, były dla Ciebie przydatne. Daj nam znać, co o tym myślisz. Jeśli zdecydujesz się użyć którejś z tych metod w swojej pracy, możesz mnie znaleźć na Twitterze pod adresem @DavidDarnes. Możesz też znaleźć Nordhealth @NordhealthHQ na Twitterze, a także resztę mojego zespołu, który ciężko pracował nad stworzeniem tego systemu projektowego i wdrożeniem funkcji wymienionych w tym artykule: @Viljamis, @WickyNilliams i @eric_habich.

Baner powitalny autorstwa Dan Cristian Pădureț