Vlastní prvky

Můžeme vytvořit vlastní HTML elementy, popsané naší třídou, s jejími vlastními metodami a vlastnostmi, událostmi a tak dále.

Jakmile je vlastní prvek definován, můžeme jej používat na stejné úrovni jako vestavěné prvky HTML.

To je skvělé, protože slovník HTML je bohatý, ale ne nekonečný. Neexistují žádné <easy-tabs> , <sliding-carousel> , <beautiful-upload> … Jen si vzpomeňte na jakoukoli jinou značku, kterou bychom mohli potřebovat.

Můžeme je definovat pomocí speciální třídy a poté je používat, jako by byly vždy součástí HTML.

Existují dva druhy vlastních prvků:

  1. Autonomní vlastní prvky – „zcela nové“ prvky rozšiřující abstraktní HTMLElement třída.
  2. Přizpůsobené vestavěné prvky – rozšíření vestavěných prvků, jako je přizpůsobené tlačítko, založené na HTMLButtonElement atd.

Nejprve pokryjeme autonomní prvky a poté přejdeme k přizpůsobeným vestavěným.

Abychom vytvořili vlastní prvek, musíme o něm prohlížeči sdělit několik podrobností:jak jej zobrazit, co dělat, když je prvek přidán nebo odebrán na stránku atd.

To se provádí vytvořením třídy se speciálními metodami. To je snadné, protože existuje jen několik metod a všechny jsou volitelné.

Zde je náčrt s úplným seznamem:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // element created
  }

  connectedCallback() {
    // browser calls this method when the element is added to the document
    // (can be called many times if an element is repeatedly added/removed)
  }

  disconnectedCallback() {
    // browser calls this method when the element is removed from the document
    // (can be called many times if an element is repeatedly added/removed)
  }

  static get observedAttributes() {
    return [/* array of attribute names to monitor for changes */];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // called when one of attributes listed above is modified
  }

  adoptedCallback() {
    // called when the element is moved to a new document
    // (happens in document.adoptNode, very rarely used)
  }

  // there can be other element methods and properties
}

Poté musíme zaregistrovat prvek:

// let the browser know that <my-element> is served by our new class
customElements.define("my-element", MyElement);

Nyní pro všechny prvky HTML se značkou <my-element> , instance MyElement je vytvořen a jsou volány výše uvedené metody. Můžeme také document.createElement('my-element') v JavaScriptu.

Název vlastního prvku musí obsahovat spojovník -

Název vlastního prvku musí obsahovat spojovník - , např. my-element a super-button jsou platná jména, ale myelement není.

To má zajistit, aby nedocházelo ke konfliktům názvů mezi vestavěnými a vlastními prvky HTML.

Příklad:„časově formátované“

Například již existuje <time> prvek v HTML pro datum/čas. Sama však neprovádí žádné formátování.

Pojďme vytvořit <time-formatted> prvek, který zobrazuje čas v pěkném formátu s ohledem na jazyk:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>
  1. Třída má pouze jednu metodu connectedCallback() – prohlížeč jej zavolá, když <time-formatted> prvek je přidán na stránku (nebo když jej analyzátor HTML detekuje) a používá vestavěný formátovač dat Intl.DateTimeFormat, který je dobře podporován ve všech prohlížečích, aby ukázal pěkně zformátovaný čas.
  2. Potřebujeme zaregistrovat náš nový prvek do customElements.define(tag, class) .
  3. A pak to můžeme použít všude.
Upgrade vlastních prvků

Pokud prohlížeč narazí na jakékoli <time-formatted> prvky před customElements.define , to není chyba. Ale prvek je zatím neznámý, stejně jako jakýkoli nestandardní tag.

Takové „nedefinované“ prvky lze stylovat pomocí CSS selektoru :not(:defined) .

Když customElement.define jsou „upgradovány“:nová instance TimeFormatted je vytvořen pro každý a connectedCallback je nazýván. Stávají se :defined .

Chcete-li získat informace o vlastních prvcích, existují metody:

  • customElements.get(name) – vrátí třídu pro vlastní prvek s daným name ,
  • customElements.whenDefined(name) – vrátí příslib, který se vyřeší (bez hodnoty), když vlastní prvek s daným name se definuje.
Vykreslování v connectedCallback , nikoli v constructor

Ve výše uvedeném příkladu je obsah prvku vykreslen (vytvořen) v connectedCallback .

Proč ne v constructor ?

Důvod je jednoduchý:když constructor říká, je ještě příliš brzy. Prvek je vytvořen, ale prohlížeč v této fázi ještě nezpracoval/přiřadil atributy:volání getAttribute vrátí null . Takže tam opravdu vykreslit nemůžeme.

Kromě toho, když se nad tím zamyslíte, je to lepší z hlediska výkonu – odložit práci, dokud to nebude skutečně potřeba.

connectedCallback spouští, když je prvek přidán do dokumentu. Nejen, že se jako dítě připojí k jinému prvku, ale ve skutečnosti se stane součástí stránky. Můžeme tedy postavit oddělený DOM, vytvořit prvky a připravit je pro pozdější použití. Budou skutečně vykresleny, až když se dostanou na stránku.

Pozorování atributů

V aktuální implementaci <time-formatted> , po vykreslení prvku nemají další změny atributů žádný účinek. To je u prvku HTML zvláštní. Obvykle, když změníme atribut, například a.href , očekáváme, že změna bude okamžitě viditelná. Takže to napravíme.

Atributy můžeme pozorovat poskytnutím jejich seznamu v observedAttributes() statický getr. Pro takové atributy attributeChangedCallback se volá, když jsou změněny. Nespouští se pro jiné, neuvedené atributy (to je z důvodů výkonu).

Zde je nový <time-formatted> , který se automaticky aktualizuje při změně atributů:

<script>
class TimeFormatted extends HTMLElement {

  render() { // (1)
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

  connectedCallback() { // (2)
    if (!this.rendered) {
      this.render();
      this.rendered = true;
    }
  }

  static get observedAttributes() { // (3)
    return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
  }

  attributeChangedCallback(name, oldValue, newValue) { // (4)
    this.render();
  }

}

customElements.define("time-formatted", TimeFormatted);
</script>

<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted>

<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
  1. Logika vykreslování je přesunuta na render() pomocná metoda.
  2. Zavoláme jej jednou, když je prvek vložen do stránky.
  3. Pro změnu atributu uvedeného v observedAttributes() , attributeChangedCallback spouštěče.
  4. …a znovu vykreslí prvek.
  5. Nakonec můžeme snadno vytvořit živý časovač.

Pořadí vykreslování

Když analyzátor HTML vytvoří DOM, prvky jsou zpracovány jeden po druhém, rodiče před dětmi. Např. pokud máme <outer><inner></inner></outer> a poté <outer> Nejprve se vytvoří a připojí prvek DOM a poté <inner> .

To vede k důležitým důsledkům pro vlastní prvky.

Pokud se například vlastní prvek pokusí o přístup k innerHTML v connectedCallback , nezíská nic:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    alert(this.innerHTML); // empty (*)
  }

});
</script>

<user-info>John</user-info>

Pokud jej spustíte, alert je prázdný.

To je přesně proto, že na této scéně nejsou žádné děti, DOM je nedokončený. Analyzátor HTML připojil vlastní prvek <user-info> , a bude pokračovat ke svým dětem, ale zatím to neudělal.

Pokud bychom chtěli předat informace vlastnímu prvku, můžeme použít atributy. Jsou okamžitě k dispozici.

Nebo, pokud děti opravdu potřebujeme, můžeme k nim odložit přístup s nulovým zpožděním setTimeout .

Toto funguje:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    setTimeout(() => alert(this.innerHTML)); // John (*)
  }

});
</script>

<user-info>John</user-info>

Nyní alert v řádku (*) ukazuje „John“, jak jej spouštíme asynchronně, po dokončení analýzy HTML. V případě potřeby můžeme zpracovat děti a dokončit inicializaci.

Na druhou stranu toto řešení také není dokonalé. Pokud jsou vnořené vlastní prvky také používají setTimeout aby se inicializovaly, pak se řadí do fronty:vnější setTimeout nejprve spouští a poté vnitřní.

Vnější prvek tedy dokončí inicializaci před vnitřním.

Ukažme si to na příkladu:

<script>
customElements.define('user-info', class extends HTMLElement {
  connectedCallback() {
    alert(`${this.id} connected.`);
    setTimeout(() => alert(`${this.id} initialized.`));
  }
});
</script>

<user-info id="outer">
  <user-info id="inner"></user-info>
</user-info>

Výstupní pořadí:

  1. vnější připojení.
  2. vnitřně připojeno.
  3. vnější inicializován.
  4. vnitřní inicializováno.

Jasně vidíme, že vnější prvek dokončí inicializaci (3) před vnitřní (4) .

Neexistuje žádné vestavěné zpětné volání, které se spustí poté, co jsou vnořené prvky připraveny. V případě potřeby můžeme takovou věc implementovat sami. Vnitřní prvky mohou například odesílat události jako initialized a vnější je mohou poslouchat a reagovat na ně.

Přizpůsobené vestavěné prvky

Nové prvky, které vytváříme, například <time-formatted> , nemají žádnou přidruženou sémantiku. Vyhledávače je neznají a zařízení pro usnadnění je nedokážou zpracovat.

Ale takové věci mohou být důležité. Vyhledávač by například měl zájem vědět, že ve skutečnosti zobrazujeme čas. A pokud vytváříme speciální druh tlačítka, proč znovu nepoužít stávající <button> funkčnost?

Můžeme rozšířit a přizpůsobit vestavěné prvky HTML zděděním z jejich tříd.

Například tlačítka jsou instancemi HTMLButtonElement , pojďme na tom stavět.

  1. Rozšířit HTMLButtonElement s naší třídou:

    class HelloButton extends HTMLButtonElement { /* custom element methods */ }
  2. Zadejte třetí argument customElements.define , který určuje značku:

    customElements.define('hello-button', HelloButton, {extends: 'button'});

    Mohou existovat různé značky, které sdílejí stejnou třídu DOM, proto zadejte extends je potřeba.

  3. Chcete-li použít náš vlastní prvek, vložte na konec běžný <button> tag, ale přidejte is="hello-button" k tomu:

    <button is="hello-button">...</button>

Zde je úplný příklad:

<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener('click', () => alert("Hello!"));
  }
}

customElements.define('hello-button', HelloButton, {extends: 'button'});
</script>

<button is="hello-button">Click me</button>

<button is="hello-button" disabled>Disabled</button>

Naše nové tlačítko rozšiřuje to vestavěné. Zachovává si tedy stejné styly a standardní funkce jako disabled atribut.

Odkazy

  • Životní standard HTML:https://html.spec.whatwg.org/#custom-elements.
  • Kompatibilita:https://caniuse.com/#feat=custom-elementsv1.

Shrnutí

Vlastní prvky mohou být dvou typů:

  1. „Autonomní“ – nové značky, rozšiřující HTMLElement .

    Schéma definice:

    class MyElement extends HTMLElement {
      constructor() { super(); /* ... */ }
      connectedCallback() { /* ... */ }
      disconnectedCallback() { /* ... */  }
      static get observedAttributes() { return [/* ... */]; }
      attributeChangedCallback(name, oldValue, newValue) { /* ... */ }
      adoptedCallback() { /* ... */ }
     }
    customElements.define('my-element', MyElement);
    /* <my-element> */
  2. „Vlastní vestavěné prvky“ – rozšíření stávajících prvků.

    Vyžaduje ještě jeden .define argument a is="..." v HTML:

    class MyButton extends HTMLButtonElement { /*...*/ }
    customElements.define('my-button', MyElement, {extends: 'button'});
    /* <button is="my-button"> */

Vlastní prvky jsou mezi prohlížeči dobře podporovány. Existuje polyfill https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs.