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ů:
- Autonomní vlastní prvky – „zcela nové“ prvky rozšiřující abstraktní
HTMLElement
třída. - 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 -
, 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>
- 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. - Potřebujeme zaregistrovat náš nový prvek do
customElements.define(tag, class)
. - A pak to můžeme použít všude.
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ýmname
,customElements.whenDefined(name)
– vrátí příslib, který se vyřeší (bez hodnoty), když vlastní prvek s danýmname
se definuje.
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>
- Logika vykreslování je přesunuta na
render()
pomocná metoda. - Zavoláme jej jednou, když je prvek vložen do stránky.
- Pro změnu atributu uvedeného v
observedAttributes()
,attributeChangedCallback
spouštěče. - …a znovu vykreslí prvek.
- 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í:
- vnější připojení.
- vnitřně připojeno.
- vnější inicializován.
- 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.
-
Rozšířit
HTMLButtonElement
s naší třídou:class HelloButton extends HTMLButtonElement { /* custom element methods */ }
-
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. -
Chcete-li použít náš vlastní prvek, vložte na konec běžný
<button>
tag, ale přidejteis="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ů:
-
„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> */
-
„Vlastní vestavěné prvky“ – rozšíření stávajících prvků.
Vyžaduje ještě jeden
.define
argument ais="..."
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.