Utilizzare gli elementi personalizzati

Boris Smus
Boris Smus

Introduzione

Il web manca di espressione. Per capire cosa intendo, dai un'occhiata a un'app web "moderna" come Gmail:

Gmail

La zuppa di <div> non è affatto moderna. Eppure, è così che creiamo le app web. È triste. Non dovremmo pretendere di più dalla nostra piattaforma?

Markup sexy. Let's make it a thing

L'HTML ci offre uno strumento eccellente per strutturare un documento, ma il suo vocabolario è limitato agli elementi definiti dallo standard HTML.

E se il markup per Gmail non fosse orribile? E se fosse bello:

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

Che bello! Anche questa app ha perfettamente senso. È significativo, facile da capire e, soprattutto, gestibile. Io/tu in futuro saprai esattamente cosa fa solo esaminando la sua struttura di base dichiarativa.

Per iniziare

Gli elementi personalizzati consentono agli sviluppatori web di definire nuovi tipi di elementi HTML. La specifica è una delle diverse nuove primitive API che rientrano nell'ambito di Web Components, ma è probabilmente la più importante. I componenti web non esistono senza le funzionalità sbloccate dagli elementi personalizzati:

  1. Definire nuovi elementi HTML/DOM
  2. Creare elementi che si estendono da altri elementi
  3. Raggruppare logicamente le funzionalità personalizzate in un unico tag
  4. Estendere l'API degli elementi DOM esistenti

Registrazione di nuovi elementi

Gli elementi personalizzati vengono creati utilizzando document.registerElement():

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

Il primo argomento di document.registerElement() è il nome del tag dell'elemento. Il nome deve contenere un trattino (-). Ad esempio, <x-tags>, <my-element> e <my-awesome-app> sono tutti nomi validi, mentre <tabs> e <foo_bar> non lo sono. Questa limitazione consente al parser di distinguere gli elementi personalizzati da quelli regolari, ma garantisce anche la compatibilità futura quando vengono aggiunti nuovi tag all'HTML.

Il secondo argomento è un oggetto (facoltativo) che descrive prototype dell'elemento. Qui puoi aggiungere funzionalità personalizzate (ad es. proprietà e metodi pubblici) ai tuoi elementi. Scopri di più in seguito.

Per impostazione predefinita, gli elementi personalizzati ereditano da HTMLElement. Pertanto, l'esempio precedente è equivalente a:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

Una chiamata a document.registerElement('x-foo') insegna al browser il nuovo elemento e restituisce un costruttore che puoi utilizzare per creare istanze di <x-foo>. In alternativa, se non vuoi utilizzare il costruttore, puoi utilizzare le altre tecniche di creazione di istanze degli elementi.

Estendere gli elementi

Gli elementi personalizzati ti consentono di estendere gli elementi HTML esistenti (nativi) e altri elementi personalizzati. Per estendere un elemento, devi passare registerElement() il nome e prototype dell'elemento da cui ereditare.

Estensione degli elementi nativi

Supponiamo che tu non sia soddisfatto di Mario Rossi <button>. Vuoi potenziarne le funzionalità per creare un "Mega pulsante". Per estendere l'elemento <button>, crea un nuovo elemento che eredita il prototype di HTMLButtonElement e extends il nome dell'elemento. In questo caso, "button":

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

Gli elementi personalizzati che ereditano da elementi nativi sono chiamati elementi personalizzati di estensione del tipo. Ereditano da una versione specializzata di HTMLElement per indicare che "l'elemento X è un Y".

Esempio:

<button is="mega-button">

Estendere un elemento personalizzato

Per creare un elemento <x-foo-extended> che espanda l'elemento personalizzato <x-foo>, eredita semplicemente il relativo prototipo e indica il tag da cui vuoi ereditare:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

Per ulteriori informazioni sulla creazione di prototipi di elementi, consulta la sezione Aggiunta di proprietà e metodi JS di seguito.

Come viene eseguito l'upgrade degli elementi

Ti sei mai chiesto perché il parser HTML non genera errori per i tag non standard? Ad esempio, è perfettamente felice se dichiariamo <randomtag> nella pagina. Secondo la specifica HTML:

Mi dispiace <randomtag>. Non sei standard e erediti da HTMLUnknownElement.

Lo stesso non vale per gli elementi personalizzati. Gli elementi con nomi di elementi personalizzati validi ereditano da HTMLElement. Puoi verificare questo fatto avviando la console: Ctrl + Shift + J (o Cmd + Opt + J su Mac) e incollando le seguenti righe di codice, che restituiscono true:

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

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

Elementi non risolti

Poiché gli elementi personalizzati vengono registrati dallo script utilizzando document.registerElement(), possono essere dichiarati o creati prima che la loro definizione venga registrata dal browser. Ad esempio, puoi dichiarare <x-tabs> nella pagina, ma finire per invocare document.registerElement('x-tabs') molto più tardi.

Prima che venga eseguito l'upgrade degli elementi alla loro definizione, vengono chiamati elementi non risolti. Si tratta di elementi HTML con un nome di elemento personalizzato valido, ma che non sono stati registrati.

Questa tabella potrebbe aiutarti a fare chiarezza:

Nome Eredita da Esempi
Elemento non risolto HTMLElement <x-tabs>, <my-element>
Elemento sconosciuto HTMLUnknownElement <tabs>, <foo_bar>

Crearne istanze

Le tecniche comuni per la creazione di elementi si applicano anche agli elementi personalizzati. Come per qualsiasi elemento standard, possono essere dichiarati in HTML o creati nel DOM utilizzando JavaScript.

Associazione di tag personalizzati

Dichiara:

<x-foo></x-foo>

Crea DOM in JS:

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

Utilizza l'operatore new:

var xFoo = new XFoo();
document.body.appendChild(xFoo);

Elementi di estensione di tipo di istanza

L'inizializzazione di elementi personalizzati di tipo estensione è molto simile ai tag personalizzati.

Dichiara:

<!-- <button> "is a" mega button -->
<button is="mega-button">

Crea DOM in JS:

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

Come puoi vedere, ora esiste una versione sovraccaricata di document.createElement() che prende l'attributo is="" come secondo parametro.

Utilizza l'operatore new:

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

Finora abbiamo imparato a utilizzare document.registerElement() per comunicare al browser un nuovo tag, ma non fa molto. Aggiungiamo proprietà e metodi.

Aggiunta di proprietà e metodi JS

La potenza degli elementi personalizzati è che puoi raggruppare funzionalità personalizzate con l'elemento definendo proprietà e metodi nella definizione dell'elemento. Pensa a questo come a un modo per creare un'API pubblica per il tuo elemento.

Ecco un esempio completo:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

Naturalmente esistono innumerevoli modi per costruire un prototype. Se non ami creare prototipi come questo, ecco una versione più compatta della stessa cosa:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

Il primo formato consente l'utilizzo di ES5 Object.defineProperty. Il secondo consente l'utilizzo di get/set.

Metodi di callback del ciclo di vita

Gli elementi possono definire metodi speciali per accedere a momenti interessanti della loro esistenza. Questi metodi sono denominati richiami del ciclo di vita. Ognuno ha un nome e uno scopo specifici:

Nome del callback Chiamato quando
createdCallback viene creata un'istanza dell'elemento
attachedCallback un'istanza è stata inserita nel documento
detachedCallback un'istanza è stata rimossa dal documento
attributeChangedCallback(attrName, oldVal, newVal) un attributo è stato aggiunto, rimosso o aggiornato

Esempio: definizione di createdCallback() e attachedCallback() su <x-foo>:

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

Tutti i callback del ciclo di vita sono facoltativi, ma definiscili se/quando è opportuno. Ad esempio, supponiamo che l'elemento sia sufficientemente complesso e apra una connessione a IndexedDB in createdCallback(). Prima che venga rimosso dal DOM, esegui le operazioni di pulizia necessarie in detachedCallback(). Nota:non dovresti fare affidamento su questo, ad esempio se l'utente chiude la scheda, ma consideralo un possibile hook di ottimizzazione.

Un altro caso d'uso dei callback del ciclo di vita è la configurazione di ascoltatori di eventi predefiniti sull'elemento:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

Aggiunta del markup

Abbiamo creato <x-foo>, gli abbiamo assegnato un'API JavaScript, ma è vuoto. Vuoi inviarci del codice HTML da eseguire?

In questo caso, sono utili i callback del ciclo di vita. In particolare, possiamo utilizzare createdCallback() per dotare un elemento di HTML predefinito:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

L'inizializzazione di questo tag e la relativa ispezione in Strumenti per sviluppatori (fai clic con il tasto destro del mouse, seleziona Ispeziona elementi) dovrebbero mostrare:

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

Incapsulamento delle parti interne nello shadow DOM

Da solo, lo shadow DOM è uno strumento efficace per incapsulare i contenuti. Se lo utilizzi in combinazione con elementi personalizzati, il risultato è magico.

Shadow DOM offre agli elementi personalizzati:

  1. Un modo per nascondere le viscere, proteggendo così gli utenti dai dettagli cruenti dell'implementazione.
  2. Incapsulamento dello stile…senza costi aggiuntivi.

Creare un elemento da Shadow DOM è come crearne uno che visualizza il markup di base. La differenza è in createdCallback():

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

Invece di impostare .innerHTML dell'elemento, ho creato un elemento Shadow Root per <x-foo-shadowdom> e poi l'ho compilato con il markup. Se l'impostazione"Mostra DOM ombra" è attivata in DevTools, viene visualizzato un #shadow-root che può essere espanso:

<x-foo-shadowdom>
  #shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

È la radice ombra.

Creazione di elementi da un modello

I modelli HTML sono un'altra nuova primitiva dell'API che si inserisce perfettamente nel mondo degli elementi personalizzati.

Esempio: registrazione di un elemento creato da un <template> e Shadow DOM:

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

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

Queste poche righe di codice sono molto efficaci. Vediamo cosa succede:

  1. Abbiamo registrato un nuovo elemento in HTML: <x-foo-from-template>
  2. Il DOM dell'elemento è stato creato da un <template>
  3. I dettagli complicati dell'elemento vengono nascosti utilizzando Shadow DOM
  4. Shadow DOM consente l'incapsulamento dello stile dell'elemento (ad es. p {color: orange;} non colora l'intera pagina di arancione)

Molto bene!

Applicazione di stili agli elementi personalizzati

Come per qualsiasi tag HTML, gli utenti del tag personalizzato possono applicare stili con i selettori:

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

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

Applicazione di stili agli elementi che utilizzano lo shadow DOM

La tana del coniglio diventa molto più profonda quando inserisci Shadow DOM. Gli elementi personalizzati che utilizzano lo shadow DOM ne ereditano i grandi vantaggi.

Shadow DOM infonde a un elemento l'incapsulamento degli stili. Gli stili definiti in un elemento Shadow Root non vengono visualizzati nell'elemento host e non vengono visualizzati nella pagina. Nel caso di un elemento personalizzato, l'elemento stesso è l'host. Le proprietà di incapsulamento dello stile consentono inoltre agli elementi personalizzati di definire stili predefiniti per se stessi.

Gli stili shadow DOM sono un argomento molto ampio. Per saperne di più, ti consiglio alcuni dei miei altri articoli:

Prevenzione di FOUC utilizzando :unresolved

Per attenuare il FOUC, gli elementi personalizzati specificano una nuova pseudo classe CSS, :unresolved. Utilizzalo per scegliere come target gli elementi non risolti, fino al punto in cui il browser richiama createdCallback() (vedi metodi del ciclo di vita). A questo punto, l'elemento non è più irrisolto. La procedura di upgrade è completata e l'elemento è stato trasformato nella relativa definizione.

Esempio: fai apparire i tag "x-foo" quando vengono registrati:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

Tieni presente che :unresolved si applica solo agli elementi non risolti, non agli elementi che ereditano da HTMLUnknownElement (vedi Come viene eseguito l'upgrade degli elementi).

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

Cronologia e supporto del browser

Rilevamento di funzionalità

Il rilevamento delle funzionalità consiste nel verificare se esiste document.registerElement():

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

Supporto browser

document.registerElement() ha iniziato a essere disponibile dietro un flag in Chrome 27 e Firefox 23 circa. Tuttavia, la specifica è molto cambiata da allora. Chrome 31 è il primo browser a supportare effettivamente la specifica aggiornata.

Finché il supporto del browser non sarà ottimale, esiste un polyfill utilizzato da Polymer di Google e da X-Tag di Mozilla.

Che cosa è successo a HTMLElementElement?

Chi ha seguito il lavoro di standardizzazione sa che una volta esisteva <element>. Era il massimo. Potresti utilizzarlo per registrare in modo dichiarativo nuovi elementi:

<element name="my-element">
    ...
</element>

Purtroppo, si sono verificati troppi problemi di tempistica con la procedura di upgrade, casi limite e scenari simili all'Armageddon per risolvere il problema. <element> ha dovuto essere messo da parte. Ad agosto 2013, Dimitri Glazkov ha pubblicato un post su public-webapps annunciando la rimozione del progetto, almeno per il momento.

Vale la pena notare che Polymer implementa una forma dichiarativa di registrazione degli elementi con <polymer-element>. Come? Utilizza document.registerElement('polymer-element') e le tecniche descritte in Creare elementi da un modello.

Conclusione

Gli elementi personalizzati ci forniscono lo strumento per estendere il vocabolario di HTML, insegnargli nuovi trucchi e saltare attraverso i wormhole della piattaforma web. Se li combiniamo con le altre nuove primitive della piattaforma, come Shadow DOM e <template>, iniziamo a capire la panoramica dei componenti web. Il markup può essere di nuovo sexy.

Se vuoi iniziare a utilizzare i componenti web, ti consiglio di dare un'occhiata a Polymer. È più che sufficiente per iniziare.