Element niestandardowy to klasa zarejestrowana pod nazwą tagu za pomocą customElements.define(). Rejestracja i podstawowy cykl życia są dobrze znane. Poniższe reguły dotyczą części, które są zwykle nieprawidłowo implementowane.
Cykl życia: części, które są nieprawidłowo wdrażane
Same wywołania zwrotne są dobrze udokumentowane. Oto błędne przekonania, które powodują błędy:
connectedCallbackmoże działać więcej niż raz. Jest uruchamiany przy każdym wstawieniu, więc przeniesienie elementu powoduje jego ponowne uruchomienie. Ustawienia powinny być idempotentne – nie zakładaj, że zostaną uruchomione tylko raz.connectedMoveCallbackpomija parę rozłączenie/ponowne połączenie podczas przenoszenia. Gdy element jest przenoszony za pomocąNode.moveBefore(), to wywołanie zwrotne jest uruchamiane zamiastdisconnectedCallback→connectedCallback, dzięki czemu stan, zaznaczenie i odtwarzanie elementu iframe lub multimediów są zachowywane po przeniesieniu. Zdefiniuj go, gdy konfiguracja lub demontaż są kosztowne lub widocznie uruchamiają się ponownie.- Demontaż w:
disconnectedCallback. Usuń globalne detektory, liczniki czasu i obserwatory, aby uniknąć wycieku. Element może jednak zostać ponownie wstawiony, więc nie traktuj usunięcia jako trwałego. - Wywołaj
super()jako pierwsze w konstruktorze i nie polegaj na obecności pełnego modelu DOM. Wywołanie metodysuper()po dotknięciuthispowoduje błądReferenceError. Atrybuty są zwykle (ale nie zawsze) obecne. Długie listy elementów podrzędnych mogą nie zostać przeanalizowane. UżyjconnectedCallbackdo jednorazowej inicjalizacji,slotchangedo monitorowania bezpośrednich elementów podrzędnych iattributeChangedCallbackdo monitorowania atrybutów. ZdarzenieconnectedCallbackmoże zostać wywołane, zanim wszystkie elementy podrzędne zostaną przeanalizowane, dlatego nie ignoruj kolejnych wywołańslotchange/attributeChangedCallback. attributeChangedCallbackjest uruchamiana tylko dlaobservedAttributes, i powinna być tania w przypadku atrybutów, które nie wpływają na dane wyjściowe.- Zaczepy cyklu życia i
observedAttributessą rejestrowane w momencie rejestracji.customElements.define()odczytuje oba tylko raz. Dodanie zaczepu lub rozszerzenieobservedAttributesw klasie po wywołaniu jest ignorowane bez powiadomienia. W szczególnościattributeChangedCallbacknigdy nie jest wywoływany, chyba żeobservedAttributesbył już obecny w czasiedefine().
Termin przejścia i :defined
Element może istnieć w DOM przed zarejestrowaniem jego klasy (w kodzie HTML renderowanym przez serwer lub gdy skrypt ładuje się późno). Dopóki nie zostanie zdefiniowany, jest to nieaktywny element HTMLElement, który nie ma żadnego z Twoich zachowań. Gdy uruchomisz customElements.define(), przeglądarka uaktualni każdy pasujący element, który znajduje się już w dokumencie, uruchamiając jego konstruktor, a następnie connectedCallback.
Modele często pomijają 2 konsekwencje:
- Atrybuty i elementy podrzędne mogą już być obecne, gdy konstruktor/
connectedCallbackjest uruchamiany podczas uaktualniania. Odczytywanie istniejących atrybutów w elemencieconnectedCallbackw celu zsynchronizowania stanu początkowego i nigdy nie zakładaj, że element był początkowo pusty. - Elementy podrzędne mogą nadal nie być dostępne nawet w
connectedCallback– parser HTML może zwrócić wynik w trakcie analizowania długich list elementów podrzędnych, więc wywołanie zwrotne jest czasami uruchamiane, zanim zostaną dołączone wszystkie elementy podrzędne. Wzorzec (slotchange+attributeChangedCallback) znajdziesz w sekcji Cykl życia powyżej.
Ukryj niezdefiniowane elementy, aby uniknąć wyświetlania treści bez stylów, ale nie blokuj dostępu, aby treści były dostępne, jeśli skrypt nigdy się nie załaduje. Animuj opacity z opóźnieniem, zamiast ukrywać na stałe za pomocą visibility: hidden, aby element pojawiał się po upływie limitu czasu, nawet jeśli nigdy nie zostanie uaktualniony:
my-element:not(:defined) {
opacity: 0;
/* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }
Zastosuj te zabezpieczenia do elementów, które ich potrzebują, lub do konkretnego poddrzewa za pomocą :has(:not(:defined)), a nie do całego body. Jeden powolny, niekrytyczny widżet (np. przycisk udostępniania) nie powinien opóźniać renderowania całej strony.
Odzwierciedlenie atrybutu ⇄ właściwości
Zsynchronizuj atrybut HTML i właściwość JS, aby element był użyteczny zarówno w znacznikach, jak i skryptach. Wartości atrybutów są zawsze ciągami tekstowymi, więc należy je przekształcić.
class MyToggle extends HTMLElement {
static observedAttributes = ['label', 'count', 'disabled'];
// String attribute reflected via getter/setter.
get label() { return this.getAttribute('label') ?? ''; }
set label(v) {
v ? this.setAttribute('label', v) : this.removeAttribute('label');
}
// Numeric attribute: coerce from string on read, serialize on write.
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', String(v)); }
// Boolean attribute: present = true, absent = false. Never set it to "false".
get disabled() { return this.hasAttribute('disabled'); }
set disabled(on) { this.toggleAttribute('disabled', !!on); }
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
}
Gdzie znajduje się wartość
W powyższym przykładzie DOM jest źródłem informacji – każda metoda pobierająca odczytuje getAttribute. To właściwe ustawienie domyślne dla kilku prostych atrybutów typu ciąg znaków: stan może znajdować się tylko w jednym miejscu, więc nie może dojść do desynchronizacji.
W przypadku bardziej rozbudowanych komponentów przechowuj źródło prawdy w polu JS i odzwierciedlaj je na zewnątrz w atrybucie tylko wtedy, gdy coś innego musi je widzieć – np. kierowanie CSS lub czytający je konsument. Odczyt pola jest znacznie tańszy niż analizowanie i wymuszanie atrybutu przy każdym dostępie, a wartości inne niż ciągi znaków nigdy nie są przekazywane w obie strony przez serializację. Dlatego biblioteki takie jak Lit przechowują stan w JS i odzwierciedlają go selektywnie, zamiast traktować atrybuty jako pamięć: odzwierciedlają tylko to, co musi być widoczne na zewnątrz, a nie każdą właściwość.
Problemy:
observedAttributes– dyscyplina: podaj tylko te atrybuty, których zmiana musi spowodować ponowne renderowanie lub wywołać efekt uboczny. Obserwowanie wszystkiego dodaje wywołanie zwrotne do każdej mutacji bez żadnych korzyści.- Pętle refleksji: metoda ustawiająca, która zapisuje atrybut, ponownie wejdzie do
attributeChangedCallback. Zabezpiecz za pomocą sprawdzeniaoldVal !== newVal, jak powyżej. - Stan tylko do stylizacji: w przypadku stanu wewnętrznego, który istnieje tylko na potrzeby stylizacji (ładowanie, aktywny, rozwinięty), preferuj stany niestandardowe zamiast odzwierciedlania atrybutu syntetycznego – patrz Stany niestandardowe poniżej. Odzwierciedlaj atrybut tylko wtedy, gdy użytkownicy muszą mieć możliwość ustawienia stanu za pomocą znaczników.
Stany niestandardowe
Stany niestandardowe to flagi logiczne, które element udostępnia o sobie, aby CSS mógł je kierować. Są one ustawiane za pomocą ElementInternals i dopasowywane do pseudoklasy :state(). Używaj ich do wewnętrznego stanu interfejsu użytkownika w czasie działania – busy, expanded, invalid – zamiast odzwierciedlać atrybut syntetyczny: nie mogą kolidować z atrybutami klienta, nigdy nie pojawiają się w DOM i są tylko do odczytu z zewnątrz (klient może stylizować :state(), ale nie może go sfałszować).
class AsyncButton extends HTMLElement {
#internals;
#action = async () => {};
constructor() {
super();
this.#internals = this.attachInternals();
}
set action(fn) { this.#action = fn; } // host supplies the work to run
async run() {
if (this.#internals.states.has('busy')) return; // guard re-entry
this.#internals.states.add('busy');
this.#internals.states.delete('success');
try {
await this.#action();
this.#internals.states.add('success');
} catch {
this.#internals.states.add('error');
} finally {
this.#internals.states.delete('busy');
}
}
}
CustomStateSet to zbiór, więc niezależne stany są łączone (busy plus error) bez tworzenia połączonego typu wyliczeniowego data-status.
Stan niestandardowy jest dostępny tylko w czasie działania. Nie jest on serializowany do modelu DOM, więc nie przetrwa ponownego wczytania, ponownego utworzenia elementu ani renderowania po stronie serwera. Jeśli stan musi być ustawiony deklaratywnie w znacznikach lub zachowywany między wczytaniami, jest to zadanie dla atrybutu (patrz Gdzie znajduje się wartość) – przywróć go w connectedCallback ze źródła informacji. Zasada: stan niestandardowy dla tego, co komponent uzyskuje i posiada w czasie działania i co wymaga jedynie stylu; atrybut dla tego, co musi być ustawione ze znaczników lub zserializowane.
Informacje o stylizowaniu tych stanów – zarówno wewnątrz komponentu, jak i w arkuszu stylów klienta – znajdziesz w artykule Stylizowanie komponentów internetowych.
Konwencje nazewnictwa
- Stan prawdziwie prywatny: pola natywne
#(#count) do twardej enkapsulacji; prefiks_tylko wtedy, gdy podklasy muszą nadal mieć do niego dostęp. Pola#nie są dziedziczone – podklasa nie widzi pól prywatnych klasy bazowej. - Właściwości z kluczami symboli są rozwiązaniem pośrednim: są ukryte przed zwykłym wyliczaniem, ale dostępne dla każdego kodu, który zawiera symbol (wyeksportuj go lub udostępnij jako pole
staticw klasie). Każdy symbol jest unikalny, więc 2 symbole o tym samym opisie nigdy nie kolidują ze sobą. Jest to przydatne, gdy podklasa lub element równorzędny potrzebuje kontrolowanego dostępu, którego nie mogą zapewnić pola#. - Nie przesłaniaj atrybutów globalnych (
style,class,id,slot,part,title,lang,dir) własnymi właściwościami, ponieważ spowoduje to zakłócenie ich wbudowanego działania. Uwaga: atrybutdisabledjest globalny tylko w przypadku elementów sterujących formularza. W przypadku innych elementów musisz samodzielnie zaimplementować jego efekt.
Podczas nazywania nowej powierzchni interfejsu API postępuj zgodnie z konwencjami platformy: używaj małych liter i stylu kebab-case z co najmniej 1 łącznikiem w przypadku nazw tagów elementów niestandardowych (np. my-toggle), lowerCamelCase w przypadku właściwości i metod oraz małych liter w przypadku nazw atrybutów zsynchronizowanych z odpowiadającymi im właściwościami.