Aangepaste elementen

Een aangepast element is een klasse die is geregistreerd voor een tagnaam via customElements.define(). Registratie en de basislevenscyclus zijn duidelijk. De onderstaande regels zijn gericht op de onderdelen die vaak onjuist worden geïmplementeerd.

Levenscyclus: de onderdelen die verkeerd worden geïmplementeerd

De callbacks zelf zijn goed gedocumenteerd. Dit zijn de misvattingen die bugs veroorzaken:

  • connectedCallback kan meerdere keren worden uitgevoerd. De gebeurtenis wordt geactiveerd bij elke invoeging, dus als u het element verplaatst, wordt de gebeurtenis opnieuw uitgevoerd. Maak de installatie idempotent: ga er niet van uit dat de installatie maar één keer wordt uitgevoerd.
  • connectedMoveCallback slaat het ontkoppelen/opnieuw koppelen over bij een verplaatsing. Als een element wordt verplaatst met Node.moveBefore(), wordt deze callback geactiveerd in plaats van disconnectedCallback → connectedCallback, zodat de status, focus en iframe-/mediaweergave de verplaatsing overleven. Definieer deze als je instelling of afbraak duur is of merkbaar opnieuw wordt uitgevoerd.
  • Afbraak over disconnectedCallback. Verwijder globale listeners, timers en observers, anders lek je. Het element kan opnieuw worden ingevoegd, dus beschouw de afbraak niet als permanent.
  • Roep super() eerst aan in de constructor en vertrouw er niet op dat de volledige DOM aanwezig is. Als je super() aanroept nadat je this hebt aangeraakt, wordt een ReferenceError gegenereerd. Kenmerken zijn meestal (maar niet altijd) aanwezig. Lange lijsten met onderliggende elementen zijn misschien nog niet helemaal geparseerd. Gebruik connectedCallback voor eenmalige initialisatie, slotchange om direct onderliggende elementen te controleren en attributeChangedCallback om kenmerken te controleren. connectedCallback kan nog steeds worden geactiveerd voordat alle onderliggende elementen zijn geparseerd. Negeer daarom geen volgende slotchange- / attributeChangedCallback-aanroepen.
  • attributeChangedCallback wordt alleen geactiveerd voor observedAttributes, en moet goedkoop blijven voor kenmerken die geen invloed hebben op de uitvoer.
  • Er worden snapshots van levenscyclus-hooks en observedAttributes gemaakt bij de registratie. customElements.define() leest beide één keer. Als je een hook toevoegt of observedAttributes uitbreidt in de klasse na de aanroep, wordt dit genegeerd. attributeChangedCallback wordt nooit geactiveerd, tenzij observedAttributes al aanwezig was op define().

Upgradetiming en :defined

Een element kan in de DOM bestaan voordat de klasse is geregistreerd (in door de server gerenderde HTML of als het script laat wordt geladen). Totdat het is gedefinieerd, is het een inerte HTMLElement zonder jouw gedrag. Als customElements.define() wordt uitgevoerd, upgradet de browser elk overeenkomend element dat al in het document staat, voert de constructor uit en daarna connectedCallback.

2 gevolgen die modellen vaak missen:

  1. Kenmerken en onderliggende elementen kunnen al aanwezig zijn wanneer je constructor/connectedCallback wordt uitgevoerd bij een upgrade. Lees bestaande kenmerken in connectedCallback om de beginstatus te synchroniseren en ga er nooit van uit dat het element leeg is begonnen.
  2. Kinderen kunnen nog steeds ontbreken, zelfs in connectedCallback. De HTML-parser kan halverwege de parse van lange lijsten met kinderen opleveren, dus de callback wordt soms uitgevoerd voordat elk kind is gekoppeld. Bekijk het opsommingsteken voor de constructor in Levenscyclus hierboven voor het patroon (slotchange + attributeChangedCallback).

Verberg niet-gedefinieerde elementen om een flits van niet-opgemaakte content te voorkomen, maar fail open, zodat de content bereikbaar blijft als het script nooit wordt geladen. Animate opacity with a delay rather than hard-hiding with visibility: hidden, so the element reveals itself after a timeout even when it is never upgraded:

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; } }

Beperk deze beveiligingen tot de elementen die ze nodig hebben, of tot een specifieke substructuur via :has(:not(:defined)), in plaats van de hele body. Eén langzame, niet-kritieke widget (bijvoorbeeld een deelknop) mag de weergave van de hele pagina niet vertragen.

Kenmerk ⇄ propertyreflectie

Houd het HTML-kenmerk en de JS-property gesynchroniseerd, zodat het element kan worden gebruikt vanuit zowel de opmaak als het script. Kenmerkwaarden zijn altijd tekenreeksen. Dwing ze af.

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();
  }
}

Waar de waarde zit

In het voorbeeld hierboven blijft het DOM de bron van waarheid: elke getter leest getAttribute. Dat is de juiste standaardinstelling voor een handvol eenvoudige, tekenreeksachtige kenmerken: er is maar één plaats waar de status kan bestaan, dus deze kan niet niet-gesynchroniseerd raken.

Voor uitgebreidere componenten houd je de bron van waarheid in een JS-veld en reflecteer je naar buiten naar een kenmerk alleen als iets anders het moet zien: CSS-targeting of een consument die het leest. Een veld lezen is veel goedkoper dan een kenmerk parseren en dwingen bij elke toegang, en niet-tekenreekswaarden worden nooit geretourneerd via serialisatie. Daarom houden bibliotheken zoals Lit de status bij in JS en reflecteren ze selectief in plaats van kenmerken als opslag te behandelen: reflecteer alleen wat de buitenwereld moet waarnemen, niet elke property.

Valkuilen:

  • observedAttributes-discipline: vermeld alleen kenmerken waarvan de wijziging een neveneffect moet veroorzaken of opnieuw moet worden weergegeven. Als je alles observeert, wordt er zonder voordeel een callback aan elke mutatie toegevoegd.
  • Reflectielussen: een setter die een kenmerk schrijft, voert attributeChangedCallback opnieuw in. Beveilig met een oldVal !== newVal-check, zoals hierboven.
  • Alleen-stijlstatus: Voor een interne status die alleen bestaat om de stijl te bepalen (laden, actief, uitgevouwen), geef je de voorkeur aan aangepaste statussen in plaats van een synthetisch kenmerk te weerspiegelen. Zie Aangepaste statussen hieronder. Reflecteer alleen naar een kenmerk als consumenten de status moeten kunnen instellen via opmaak.

Aangepaste staten

Aangepaste statussen zijn booleaanse markeringen die een element over zichzelf toont voor CSS om te targeten, ingesteld via ElementInternals en overeenkomend met de pseudoklasse :state(). Gebruik ze voor interne runtime-UI-status (busy, expanded, invalid) in plaats van een synthetisch kenmerk te weerspiegelen: ze kunnen niet botsen met de kenmerken van een consument, verschijnen nooit in de DOM en zijn alleen-lezen van buitenaf (een consument kan :state() stijlen, maar niet vervalsen).

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 is een set, dus onafhankelijke staten vormen (busy plus error) zonder een gecombineerde data-status-enum te bedenken.

Aangepaste status is alleen runtime. De eigenschap wordt niet geserialiseerd naar het DOM, dus deze blijft niet behouden na opnieuw laden, opnieuw maken van het element of serverrendering. Als de status declaratief moet worden ingesteld in de opmaak of moet blijven bestaan tussen laadcycli, is dat een taak voor een kenmerk (zie Waar de waarde zich bevindt). Herstel deze in connectedCallback vanuit je bron van waarheid. Vuistregel: aangepaste status voor wat de component afleidt en bezit tijdens de runtime en alleen hoeft te stylen; een kenmerk voor wat moet worden ingesteld vanuit de opmaak of geserialiseerd.

Ga naar Styling Web Components (Webcomponenten stylen) voor meer informatie over hoe je deze statussen stijlt, zowel binnen de component als vanuit de stylesheet van een consument.

Naamconventies

  • Echt privé: native #-velden (#count) voor harde inkapseling; een _-voorvoegsel alleen als subklassen er nog steeds toegang toe moeten hebben. Let op: #-velden worden niet overgenomen. Een subklasse kan de privévelden van een basisklasse niet zien.
  • Eigenschappen met een symboolsleutel zijn een tussenweg: ze zijn verborgen voor gewone opsomming, maar bereikbaar via elke code die het symbool bevat (exporteer het of maak het beschikbaar als een static-veld in de klasse). Omdat elk symbool uniek is, botsen 2 symbolen met dezelfde beschrijving nooit. Dit is handig als een subklasse of sibling gecontroleerde toegang nodig heeft die #-velden niet kunnen bieden.
  • Maak geen schaduw van algemene kenmerken (style, class, id, slot, part, title, lang, dir) met je eigen property's. Als je dit wel doet, werkt het ingebouwde gedrag niet meer. Opmerking: disabled is alleen algemeen voor formulierbedieningselementen. Voor andere elementen moet je het effect zelf implementeren.

Volg de conventies van het platform als je een nieuwe API-surface een naam geeft: gebruik kebab-case met kleine letters en minstens één koppelteken voor aangepaste elementtagnamen (bijvoorbeeld my-toggle), lowerCamelCase voor property's en methoden, en kenmerknamen met kleine letters die zijn gesynchroniseerd met de bijbehorende property's.