Sprawdzone metody dotyczące elementów niestandardowych

Elementy niestandardowe umożliwiają tworzenie własnych tagów HTML. Ta lista kontrolna zawiera sprawdzone metody tworzenia elementów wysokiej jakości.

Elementy niestandardowe umożliwiają rozszerzanie kodu HTML i definiowanie własnych tagów. Są to niezwykle przydatne funkcje, ale są też niskopoziomowe, co oznacza, że nie zawsze jest jasne, jak najlepiej wdrożyć własny element.

Aby ułatwić Ci korzystanie z tej usługi, przygotowaliśmy listę kontrolną. Obejmuje wszystkie elementy, które według nas są potrzebne, by utworzyć element niestandardowy.

Lista kontrolna

model DOM

Utwórz pierwiastek cienia, aby uwzględnić style.

Why? Kodowanie stylów w głównym katalogu cienia elementu zapewnia, że będą one działać niezależnie od tego, gdzie są używane. Jest to szczególnie ważne, gdy programista chce umieścić element w grzebu głównym cienia innego elementu. Dotyczy to nawet prostych elementów, takich jak pole wyboru czy przycisk opcji. Może się tak zdarzyć, że jedyną treścią w źródle cienia będą same style.
Przykład Element <howto-checkbox>.

Utwórz pierwiastek cienia w konstruktorze.

Why? Konstruktor polega na tym, że masz wyłączną wiedzę o danym elemencie. To dobry moment na skonfigurowanie szczegółów implementacji, aby uniknąć pomyłki nad innymi elementami. Wykonanie tej czynności w późniejszym wywołaniu zwrotnym, na przykład przy użyciu connectedCallback, oznacza, że trzeba zabezpieczyć się przed sytuacją, w której element zostanie odłączony, a następnie ponownie dołączony do dokumentu.
Przykład Element <howto-checkbox>.

Umieść wszystkie elementy podrzędne tworzone przez ten element w korzeniu cienia.

Why? Elementy podrzędne utworzone przez Twój element są częścią jego implementacji i powinny być prywatne. Bez ochrony cienia głównego kod JavaScript może nieumyślnie zakłócić działanie tych elementów.
Przykład Element <howto-tabs>.

Użyj elementu <slot>, aby wyświetlić elementy podrzędne Light DOM w modelu Shadow DOM.

Why? Zezwalaj użytkownikom komponentu na określanie treści, ponieważ elementy podrzędne HTML zwiększają jego kompozycję. Jeśli przeglądarka nie obsługuje elementów niestandardowych, zagnieżdżone treści pozostają dostępne, widoczne i dostępne.
Przykład Element <howto-tabs>.

Ustaw styl wyświetlania :host (np. block, inline-block, flex), chyba że wolisz używać domyślnego stylu inline.

Why? Domyślnie elementy niestandardowe mają wartość display: inline, więc ustawienie ich width lub height nie będzie miało żadnego efektu. Często jest to zaskoczeniem dla deweloperów i może powodować problemy z układem strony. Jeśli nie chcesz korzystać z ekranu inline, zawsze ustawiaj domyślną wartość display.
Przykład Element <howto-checkbox>.

Dodaj styl wyświetlania :host z uwzględnieniem ukrytego atrybutu.

Why? Element niestandardowy z domyślnym stylem display, np. :host { display: block }, zastąpi atrybut hidden o niższej szczegółowości. Może to być zaskoczeniem, jeśli oczekujesz ustawienia atrybutu hidden w elemencie tak, aby renderował go display: none. Oprócz domyślnego stylu display dodaj obsługę stylu hidden z użyciem atrybutu :host([hidden]) { display: none }.
Przykład Element <howto-checkbox>.

Atrybuty i właściwości

Nie zastępuj atrybutów globalnych ustawionych przez autora.

Why? Atrybuty globalne to te, które występują we wszystkich elementach HTML. Oto niektóre przykłady: tabindex i role. W przypadku elementu niestandardowego można ustawić początkową wartość tabindex na 0, aby można było go zaznaczyć za pomocą klawiatury. Zawsze jednak najpierw sprawdź, czy programista korzystający z Twojego elementu nie ustawił tej wartości na inną. Jeśli na przykład atrybut tabindex ma wartość -1, jest to sygnał, że nie chce, aby ten element był interaktywny.
Przykład Element <howto-checkbox>. Szczegółowo wyjaśniamy to w sekcji Nie zastępuj autora strony

Zawsze akceptuj dane podstawowe (ciągi, liczby, wartości logiczne) jako atrybuty lub właściwości.

Why? Elementy niestandardowe, takie jak ich wbudowane odpowiedniki, powinny być konfigurowane. Konfigurację można przekazywać deklaratywnie, za pomocą atrybutów lub imperatywnie za pomocą właściwości JavaScriptu. W idealnej sytuacji każdy atrybut powinien być również połączony z odpowiednią usługą.
Przykład Element <howto-checkbox>.

Staraj się synchronizować podstawowe atrybuty danych i ich właściwości, odzwierciedlając dane w każdej usłudze i na odwrót.

Why? Nigdy nie wiadomo, jak użytkownik wejdzie w interakcję z Twoim elementem. Może ustawić właściwość w JavaScript, a następnie odczytać tę wartość za pomocą interfejsu API takiego jak getAttribute(). Jeśli każdy atrybut ma odpowiadającą jej właściwość i oba odzwierciedlają je wszystkie, ułatwi to użytkownikom pracę z Twoim elementem. Innymi słowy, wywołanie setAttribute('foo', value) powinno też spowodować ustawienie odpowiadającej mu właściwości foo i odwrotnie. Oczywiście są wyjątki od tej reguły. Nie powinny odnosić się do właściwości o wysokiej częstotliwości, np. currentTime w odtwarzaczu. Kieruj się własną oceną Jeśli wygląda na to, że użytkownik wejdzie w interakcję z właściwością lub atrybutem, a odzwierciedlenie tego problemu nie jest męczące, zrób to.
Przykład Element <howto-checkbox>. Zostało to szczegółowo wyjaśnione w artykule Unikanie problemów z ponownym korzystaniem z usługi.

Staraj się akceptować jako właściwości tylko dane rozszerzone (obiekty, tablice).

Why? Ogólnie rzecz biorąc, nie ma przykładów wbudowanych elementów HTML, które za pomocą atrybutów akceptują dane sformatowane (zwykłe obiekty i tablice JavaScript). Dane rozszerzone są akceptowane przez wywołania metod lub właściwości. Przyjmowanie danych rozszerzonych jako atrybutów ma kilka oczywistych wad: zserializowanie dużego obiektu w ciągu znaków może być kosztowne, a w tym procesie ciągnięcia zostaną utracone wszystkie odniesienia do obiektów. Jeśli na przykład utworzysz ciąg znaków dla obiektu, który odwołuje się do innego obiektu lub węzła DOM, te odwołania zostaną utracone.

Nie odzwierciedlają właściwości rozszerzonych danych w atrybutach.

Why? Odzwierciedlenie właściwości rozszerzonych w atrybutach jest niepotrzebnie kosztowne oraz wymaga serializacji i deserializacji tych samych obiektów JavaScript. Jeśli nie masz przypadku użycia, który można rozwiązać tylko za pomocą tej funkcji, prawdopodobnie lepiej tego unikać.

Rozważ sprawdzenie, czy istnieją właściwości, które mogły zostać ustawione przed uaktualnieniem elementu.

Why? Programista korzystający z Twojego elementu może próbować ustawić jego właściwość, zanim zostanie wczytana jego definicja. Jest to szczególnie istotne, jeśli deweloper używa platformy, która obsługuje wczytywanie komponentów, oznaczanie ich na stronie i powiązanie ich właściwości z modelem.
Przykład Element <howto-checkbox>. Wyjaśnienie zostało szczegółowo wyjaśnione w sekcji Ustawianie właściwości jako leniwej.

Nie zgłaszaj zajęć samodzielnie.

Why? Elementy, które muszą określać stan, powinny używać atrybutów. Uznaje się, że atrybut class należy do dewelopera, który korzysta z Twojego elementu, więc pisanie w nim samodzielnie może spowodować nieumyślne użycie klas programisty.

Wydarzenia

Wysyłaj zdarzenia w odpowiedzi na wewnętrzne działanie komponentu.

Why? Właściwości komponentu mogą się zmieniać w zależności od działania, o którym wie tylko on. Może to być na przykład zakończenie wczytywania licznika czasu lub animacji albo zakończenie ładowania zasobu. Warto wysyłać zdarzenia w odpowiedzi na te zmiany, aby powiadomić hosta o innym stanie komponentu.

Nie wysyłaj zdarzeń w odpowiedzi na ustawienie usługi hosta (przepływ danych w dół).

Why? Wysyłając zdarzenie w odpowiedzi na ustawienie hosta dla usługi, jest niepotrzebne (host wie bieżący stan, ponieważ po prostu go ustawił). Wysyłanie zdarzeń w odpowiedzi na ustawienie hosta w usłudze może spowodować zapętlenie w systemach wiązania danych.
Przykład Element <howto-checkbox>.

Filmy objaśniające

Nie zastępuj autora strony

Może się zdarzyć, że programista korzystający z Twojego elementu będzie chciał zastąpić część jego początkowego stanu. Może to być na przykład zmiana role ARIA lub możliwość koncentracji za pomocą tabindex. Zanim zastosujesz własne wartości, sprawdź, czy te atrybuty globalne zostały ustawione.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Ustaw właściwości jako leniwe

Programista może próbować ustawić właściwość w elemencie, zanim zostanie wczytana jego definicję. Jest tak szczególnie wtedy, gdy deweloper używa struktury, która obsługuje wczytywanie komponentów, wstawianie ich na stronie i powiązanie ich właściwości z modelem.

W poniższym przykładzie Angular deklaratywnie wiąże właściwość isChecked swojego modelu z właściwością checked pola wyboru. Jeśli definicja pola wyboru instrukcji to leniwe ładowanie, możliwe, że Angular może spróbować ustawić zaznaczoną właściwość, zanim element zostanie uaktualniony.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Element niestandardowy powinien rozwiązać ten problem, sprawdzając, czy w jego instancji ustawiono już jakieś właściwości. <howto-checkbox> demonstruje ten wzorzec za pomocą metody _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() przechwytuje wartość z nieuaktualnionej instancji i usuwa właściwość, aby nie zasłaniała obiektu ustawiającego właściwości elementu niestandardowego. Dzięki temu po załadowaniu definicji elementu może on od razu odzwierciedlić prawidłowy stan.

Unikanie problemów związanych z ponownym przejściem

Użycie parametru attributeChangedCallback() do odzwierciedlenia stanu właściwości podstawowej, np.:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Może to jednak utworzyć nieskończoną pętlę, jeśli metoda ustawiająca również odzwierciedla atrybut.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Możesz też zezwolić funkcji ustawiającej na podstawie atrybutu na odzwierciedlenie tego atrybutu i zezwolić na ustalenie jego wartości na podstawie atrybutu.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

get checked() {
  return this.hasAttribute('checked');
}

W tym przykładzie dodanie lub usunięcie atrybutu spowoduje też ustawienie właściwości.

Poza tym attributeChangedCallback() może służyć do obsługi efektów ubocznych, takich jak stosowanie stanów ARIA.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}