Custom Elements v1 - Componenti web riutilizzabili

Gli elementi personalizzati consentono agli sviluppatori web di definire nuovi tag HTML, estendere quelli esistenti e creare componenti web riutilizzabili.

Con Elementi personalizzati, gli sviluppatori web possono creare nuovi tag HTML, migliorare i tag HTML esistenti o estendere i componenti creati da altri sviluppatori. L'API è la base dei componenti web. Offre un modo basato su standard web per creare componenti riutilizzabili utilizzando solo JS/HTML/CSS. Il risultato è meno codice, codice modulare e un maggiore riutilizzo nelle nostre app.

Introduzione

Il browser ci offre uno strumento eccellente per strutturare le applicazioni web. Si chiama HTML. Potresti averne sentito parlare. È dichiarativo, portatile, supportato e facile da usare. Per quanto sia ottimo, l'HTML ha un vocabolario e un'espandibilità limitati. Lo standard HTML non ha mai avuto un modo per associare automaticamente il comportamento JS al markup… fino ad ora.

Gli elementi personalizzati sono la risposta al rinnovamento dell'HTML, al completamento dei componenti mancanti e al raggruppamento della struttura con il comportamento. Se HTML non fornisce la soluzione a un problema, possiamo creare un elemento personalizzato che lo faccia. Gli elementi personalizzati insegnano al browser nuovi trucchi, preservando al contempo i vantaggi dell'HTML.

Definizione di un nuovo elemento

Per definire un nuovo elemento HTML, abbiamo bisogno della potenza di JavaScript.

Il tag globale customElements viene utilizzato per definire un elemento personalizzato e indicare al browser un nuovo tag. Chiama customElements.define() con il nome del tag che vuoi creare e un class JavaScript che espande la base HTMLElement.

Esempio: definizione di un riquadro del riquadro mobile, <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

Esempio di utilizzo:

<app-drawer></app-drawer>

È importante ricordare che l'utilizzo di un elemento personalizzato non è diverso dall'utilizzo di un <div> o di qualsiasi altro elemento. Le istanze possono essere dichiarate nella pagina, create dinamicamente in JavaScript, è possibile associare ascoltatori di eventi e così via. Continua a leggere per altri esempi.

Definizione dell'API JavaScript di un elemento

La funzionalità di un elemento personalizzato viene definita utilizzando un oggetto class ES2015 che estende HTMLElement. L'estensione di HTMLElement garantisce che l'elemento personalizzato erediti l'intera API DOM e che tutte le proprietà/i metodi aggiunti alla classe diventino parte dell'interfaccia DOM dell'elemento. In sostanza, utilizza la classe per creare un'API JavaScript pubblica per il tuo tag.

Esempio: definizione dell'interfaccia DOM di <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

In questo esempio, stiamo creando un riquadro con una proprietà open, una proprietà disabled e un metodo toggleDrawer(). Inoltre, rispecchia le proprietà come attributi HTML.

Una caratteristica interessante degli elementi personalizzati è che this all'interno di una definizione di classe si riferisce all'elemento DOM stesso, ovvero all'istanza della classe. Nel nostro esempio, this si riferisce a <app-drawer>. In questo modo (😉) l'elemento può collegare a se stesso un ascoltatore click. Inoltre, non sei limitato ai listener di eventi. L'intera API DOM è disponibile all'interno del codice dell'elemento. Utilizza this per accedere alle proprietà dell'elemento, ispezionare i relativi elementi secondari (this.children), i nodi di query (this.querySelectorAll('.items')) e così via.

Regole per la creazione di elementi personalizzati

  1. Il nome di un elemento personalizzato deve contenere un trattino (-). Pertanto, <x-tags>, <my-element> e <my-awesome-app> sono tutti nomi validi, mentre <tabs> e <foo_bar> non lo sono. Questo requisito consente all'interprete HTML di distinguere gli elementi personalizzati da quelli normali. Garantisce inoltre la compatibilità con le versioni future quando vengono aggiunti nuovi tag all'HTML.
  2. Non puoi registrare lo stesso tag più di una volta. Il tentativo di farlo causerà un DOMException. Una volta comunicato al browser un nuovo tag, non devi fare altro. Nessun reso.
  3. Gli elementi personalizzati non possono essere autochiudenti perché HTML consente solo a alcuni elementi di essere autochiudenti. Scrivi sempre un tag di chiusura (<app-drawer></app-drawer>).

Reazioni con elementi personalizzati

Un elemento personalizzato può definire hook speciali del ciclo di vita per l'esecuzione del codice durante periodi interessanti della sua esistenza. Si tratta delle reazioni con elementi personalizzati.

Nome Chiamato quando
constructor Viene creata o eseguita l'upgrade di un'istanza dell'elemento. Utile per inizializzare lo stato, configurare i listener di eventi o creare un dom shadow. Consulta le specifiche per conoscere le limitazioni relative a cosa puoi fare in constructor.
connectedCallback Viene chiamato ogni volta che l'elemento viene inserito nel DOM. Utile per eseguire codice di configurazione, ad esempio recupero di risorse o rendering. In genere, dovresti provare a posticipare il lavoro fino a questo momento.
disconnectedCallback Viene chiamata ogni volta che l'elemento viene rimosso dal DOM. Utile per eseguire il codice di pulizia.
attributeChangedCallback(attrName, oldVal, newVal) Viene chiamato quando un attributo osservato è stato aggiunto, rimosso, aggiornato o sostituito. Chiamato anche per i valori iniziali quando un elemento viene creato dal parser o sometido a upgrade. Nota: solo gli attributi elencati nella proprietà observedAttributes riceveranno questo callback.
adoptedCallback L'elemento personalizzato è stato spostato in un nuovo document (ad esempio, un utente di nome document.adoptNode(el)).

I callback di reazione sono sincroni. Se qualcuno chiama el.setAttribute() sul tuo elemento, il browser chiamerà immediatamente attributeChangedCallback(). Analogamente, riceverai un disconnectedCallback() subito dopo che l'elemento è stato rimosso dal DOM (ad es. l'utente chiama el.remove()).

Esempio:aggiunta di reazioni di elementi personalizzati a <app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

Definisci le reazioni se/quando è opportuno. Se l'elemento è sufficientemente complesso e apre una connessione a IndexedDB in connectedCallback(), esegui le operazioni di pulizia necessarie in connectedCallback(). Fai attenzione, però. Non puoi fare affidamento sul fatto che il tuo elemento venga rimosso dal DOM in tutte le circostanze. Ad esempio, disconnectedCallback() non verrà mai chiamato se l'utente chiude la scheda.

Proprietà e attributi

Riflettere le proprietà agli attributi

Capita spesso che le proprietà HTML riflettano il loro valore nel DOM come attributo HTML. Ad esempio, quando i valori di hidden o id vengono modificati in JS:

div.id = 'my-id';
div.hidden = true;

i valori vengono applicati al DOM live come attributi:

<div id="my-id" hidden>

Questa operazione è chiamata "riferimento delle proprietà agli attributi". Quasi tutte le proprietà in HTML lo fanno. Perché? Gli attributi sono utili anche per configurare un elemento in modo dichiarativo e alcune API come l'accessibilità e i selettori CSS si basano sugli attributi per funzionare.

Riflettere una proprietà è utile ovunque tu voglia mantenere la rappresentazione DOM dell'elemento sincronizzata con il suo stato JavaScript. Uno dei motivi per cui potresti voler riflettere una proprietà è che gli stili definiti dall'utente vengano applicati quando lo stato JS cambia.

Ricorda le nostre <app-drawer>. Un consumatore di questo componente potrebbe volerlo attenuare e/o impedire l'interazione dell'utente quando è disattivato:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

Quando la proprietà disabled viene modificata in JS, vogliamo che l'attributo venga aggiunto al DOM in modo che il selettore dell'utente corrisponda. L'elemento può fornire questo comportamento riflettendo il valore su un attributo con lo stesso nome:

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

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

Osservazione delle modifiche agli attributi

Gli attributi HTML sono un modo pratico per dichiarare lo stato iniziale:

<app-drawer open disabled></app-drawer>

Gli elementi possono reagire alle modifiche degli attributi definendo un attributeChangedCallback. Il browser chiamerà questo metodo per ogni modifica agli attributi elencati nell'array observedAttributes.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

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

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

Nell'esempio, impostiamo attributi aggiuntivi su <app-drawer> quando un attributo disabled viene modificato. Anche se non lo facciamo qui, potresti anche utilizzare attributeChangedCallback per mantenere una proprietà JS in sincronia con il relativo attributo.

Upgrade degli elementi

HTML con miglioramento progressivo

Abbiamo già appreso che gli elementi personalizzati vengono definiti chiamando customElements.define(). Tuttavia, ciò non significa che devi definire e registrare un elemento personalizzato tutto in una volta.

Gli elementi personalizzati possono essere utilizzati prima della registrazione della loro definizione.

Il miglioramento progressivo è una funzionalità degli elementi personalizzati. In altre parole, puoi dichiarare una serie di elementi <app-drawer> nella pagina e non richiamare mai customElements.define('app-drawer', ...) fino a molto tempo dopo. Questo accade perché il browser tratta i potenziali elementi personalizzati in modo diverso grazie ai tag sconosciuti. Il processo per chiamare define() e aggiungere una definizione di classe a un elemento esistente è chiamato "upgrade elementi".

Per sapere quando un nome di tag viene definito, puoi utilizzare window.customElements.whenDefined(). Restituisce una promessa che si risolve quando l'elemento viene definito.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

Esempio: ritarda il lavoro finché non viene eseguito l'upgrade di un insieme di elementi secondari

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

Contenuti definiti dagli elementi

Gli elementi personalizzati possono gestire i propri contenuti utilizzando le API DOM all'interno del codice dell'elemento. A questo scopo, puoi utilizzare le reazioni.

Esempio. Crea un elemento con codice HTML predefinito:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

La dichiarazione di questo tag produrrà:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// DA FARE: DevSite - Esempio di codice rimosso perché utilizzava gestori eventi in linea

Creazione di un elemento che utilizza Shadow DOM

Shadow DOM consente a un elemento di possedere, eseguire il rendering e applicare stili a un frammento di DOM separato dal resto della pagina. Puoi persino nascondere un'intera app in un singolo tag:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

Per utilizzare Shadow DOM in un elemento personalizzato, chiama this.attachShadow all'interno di constructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

Esempio di utilizzo:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

Testo personalizzato dell'utente

// DA FARE: DevSite - Esempio di codice rimosso perché utilizzava gestori eventi in linea

Creazione di elementi da un <template>

Per chi non lo sapesse, l'elemento <template> consente di dichiarare frammenti del DOM che vengono analizzati, sono inattivi al caricamento della pagina e possono essere attivati in un secondo momento in fase di esecuzione. Si tratta di un'altra API primitiva della famiglia di componenti web. I modelli sono un segnaposto ideale per dichiarare la struttura di un elemento personalizzato.

Esempio: registrazione di un elemento con contenuti shadow DOM creati da un <template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

Queste poche righe di codice sono molto efficaci. Cerchiamo di capire i principali aspetti:

  1. Stiamo definendo un nuovo elemento in HTML: <x-foo-from-template>
  2. Il DOM ombra dell'elemento viene creato da un <template>
  3. Il DOM dell'elemento è locale all'elemento grazie al DOM shadow
  4. Il CSS interno dell'elemento è limitato all'elemento grazie a shadow DOM

Sono nel DOM Ombra. Il mio markup è stato stampato da un <template>.

// DA FARE: DevSite - Esempio di codice rimosso perché utilizzava gestori eventi in linea

Applicare uno stile a un elemento personalizzato

Anche se l'elemento definisce uno stile proprio mediante Shadow DOM, gli utenti possono definire lo stile dell'elemento personalizzato dalla loro pagina. Questi sono chiamati "stili definiti dall'utente".

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

Ti starai chiedendo come funziona la specificità CSS se l'elemento ha stili definiti all'interno di Shadow DOM. In termini di specificità, gli stili utente sono i migliori. Rimuovereanno sempre lo stile definito dall'elemento. Consulta la sezione Creare un elemento che utilizza Shadow DOM.

Pre-styling degli elementi non registrati

Prima di eseguire l'upgrade di un elemento, puoi sceglierlo come target in CSS utilizzando la pseudo-classe :defined. Questa opzione è utile per applicare lo stile in anteprima a un componente. Per esempio, potresti voler evitare FOUC di layout o di altro tipo nascondendo i componenti non definiti e facendoli apparire gradualmente quando vengono definiti.

Esempio: nascondi <app-drawer> prima che venga definito:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

Una volta definito <app-drawer>, il selettore (app-drawer:not(:defined)) non corrisponde più.

Estendere gli elementi

L'API Custom Elements è utile per creare nuovi elementi HTML, ma è utile anche per estendere altri elementi personalizzati o persino l'HTML integrato del browser.

Estendere un elemento personalizzato

Per estendere un altro elemento personalizzato, devi estendere la relativa definizione di classe.

Esempio: crea <fancy-app-drawer> che estende <app-drawer>:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

Estensione degli elementi HTML nativi

Supponiamo che tu voglia creare un <button> più fantasioso. Anziché replicare il comportamento e la funzionalità di <button>, è preferibile migliorare progressivamente l'elemento esistente utilizzando elementi personalizzati.

Un elemento integrato personalizzato è un elemento personalizzato che estende uno dei tag HTML integrati del browser. Il vantaggio principale dell'estensione di un elemento esistente è ottenere tutte le sue funzionalità (proprietà DOM, metodi, accessibilità). Non esiste modo migliore per scrivere un'app web progressiva che migliorare progressivamente gli elementi HTML esistenti.

Per estendere un elemento, devi creare una definizione di classe che erediti dall'interfaccia DOM corretta. Ad esempio, un elemento personalizzato che estende <button> deve ereditare da HTMLButtonElement anziché da HTMLElement. Analogamente, un elemento che estende <img> deve estendere HTMLImageElement.

Esempio: estensione di <button>:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

Tieni presente che la chiamata a define() cambia leggermente quando viene esteso un elemento nativo. Il terzo parametro obbligatorio indica al browser il tag che stai ampliando. Questo è necessario perché molti tag HTML condividono la stessa interfaccia DOM. <section>, <address> e <em> (tra gli altri) condividono tuttiHTMLElement; sia <q> che <blockquote> condividono HTMLQuoteElement; e così via… La specifica di {extends: 'blockquote'} consente al browser di sapere che stai creando un<blockquote> migliorato anziché un <q>. Consulta le specifiche HTML per l'elenco completo delle interfacce DOM dell'HTML.

Gli utenti di un elemento integrato personalizzato possono utilizzarlo in diversi modi. Può dichiarare questo elemento aggiungendo l'attributo is="" al tag nativo:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

crea un'istanza in JavaScript:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

oppure utilizza l'operatore new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

Ecco un altro esempio che estende <img>.

Esempio: estensione di <img>:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

Gli utenti dichiarano questo componente come:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

oppure crea un'istanza in JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

Dettagli vari

Elementi sconosciuti ed elementi personalizzati non definiti

L'HTML è permissivo e flessibile da utilizzare. Ad esempio, se dichiari <randomtagthatdoesntexist> in una pagina, il browser lo accetta. Perché i tag non standard funzionano? La risposta è che la specifica HTML lo consente. Gli elementi non definiti dalla specifica vengono analizzati come HTMLUnknownElement.

Lo stesso non vale per gli elementi personalizzati. I potenziali elementi personalizzati vengono analizzati come HTMLElement se sono creati con un nome valido (include un "-"). Puoi verificarlo in un browser che supporta gli elementi personalizzati. Avvia la console: Ctrl+Maiusc+J (o Cmd+Opt+J su Mac) e incolla le seguenti righe di codice:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

Riferimento API

L'elemento globale customElements definisce metodi utili per lavorare con gli elementi personalizzati.

define(tagName, constructor, options)

Definisce un nuovo elemento personalizzato nel browser.

Esempio

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

Dato un nome di tag elemento personalizzato valido, restituisce il costruttore dell'elemento. Restituisce undefined se non è stata registrata alcuna definizione di elemento.

Esempio

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

Restituisce una promessa che si risolve quando l'elemento personalizzato è definito. Se l'elemento è già definito, risolvi immediatamente. Viene rifiutato se il nome del tag non è un nome di elemento personalizzato valido.

Esempio

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

Supporto per cronologia e browser

Se segui i componenti web negli ultimi due anni, saprai che Chrome 36 e versioni successive hanno implementato una versione dell'API Custom Elements che utilizza document.registerElement() anziché customElements.define(). Ora è considerata una versione non più supportata dello standard, chiamata v0. customElements.define() è la novità e ciò che i fornitori di browser stanno iniziando a implementare. Si chiama Elementi personalizzati v1.

Se ti interessa la specifica v0 precedente, consulta l'articolo html5rocks.

Supporto browser

Chrome 54 (stato), Safari 10.1 (stato) e Firefox 63 (stato) includono Elementi personalizzati 1. Lo sviluppo di Edge è iniziato.

Per rilevare le funzionalità degli elementi personalizzati, verifica l'esistenza di window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

Fino a quando il supporto del browser non sarà ampiamente disponibile, è disponibile un polyfill autonomo per Elementi personalizzati 1. Tuttavia, consigliamo di utilizzare il loader webcomponents.js per caricare in modo ottimale i polyfill dei componenti web. Il caricatore utilizza il rilevamento delle funzionalità per caricare in modo asincrono solo i pollyfill necessari richiesti dal browser.

Installalo:

npm install --save @webcomponents/webcomponentsjs

Utilizzo:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

Conclusione

Gli elementi personalizzati ci offrono un nuovo strumento per definire nuovi tag HTML nel browser e creare componenti riutilizzabili. Se li combiniamo con le altre nuove primitive della piattaforma, come Shadow DOM e <template>, iniziamo a capire il quadro generale dei componenti web:

  • Supporto multibrowser (standard web) per la creazione ed estensione di componenti riutilizzabili.
  • Non richiede librerie o framework per iniziare. Vanilla JS/HTML FTW!
  • Fornisce un modello di programmazione familiare. Si tratta solo di DOM/CSS/HTML.
  • Funziona bene con altre nuove funzionalità della piattaforma web (Shadow DOM, <template>, proprietà personalizzate CSS e così via)
  • Perfettamente integrato con DevTools del browser.
  • Sfrutta le funzionalità di accessibilità esistenti.