Utilizzare gli elementi personalizzati

Boris Smus
Boris Smus

Introduzione

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

Gmail

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

Markup sexy. Diamoci da fare

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 bellissimo:

<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 rinfrescante! Anche quest'app ha senso. È significativo, facile da capire e, soprattutto, può essere gestito. Io/tu in futuro sapremo 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. Estendi 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 per document.registerElement() è il nome tag dell'elemento. Il nome deve contenere un trattino (-). Ad esempio, <x-tags>, <my-element> e <my-awesome-app> sono nomi validi, mentre <tabs> e <foo_bar> non lo sono. Questa restrizione consente all'analizzatore sintattico di distinguere gli elementi personalizzati da quelli normali, ma garantisce anche la compatibilità in avanti quando vengono aggiunti nuovi tag al codice 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. Approfondiremo l'argomento più avanti.

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 per creare un'istanza degli elementi.

Estendere gli elementi

Gli elementi personalizzati 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 Uomo medio <button>. Vorresti sfruttarne al meglio le funzionalità per diventare un "pulsante Mega". Per estendere l'elemento <button>, crea un nuovo elemento che eredita 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 di tipo. Ereditano da una versione specializzata di HTMLElement per indicare che "l'elemento X è un Y".

Esempio:

<button is="mega-button">

Estensione di 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 esegui l'ereditarietà:

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

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

Per saperne di più sulla creazione di prototipi di elementi, consulta la sezione Aggiungere proprietà e metodi JS di seguito.

Come viene eseguito l'upgrade degli elementi

Ti sei mai chiesto perché l'interprete HTML non segnala i tag non standard? Ad esempio, se dichiariamo <randomtag> nella pagina, è perfetto. Secondo la specifica HTML:

Spiacenti, <randomtag>. Non sei uno standard ed eredita da HTMLUnknownElement.

Lo stesso non vale per gli elementi personalizzati. Gli elementi con nomi 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 per 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 chiarire le cose:

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

Creare istanze di elementi

Le tecniche comuni di creazione degli elementi si applicano anche agli elementi personalizzati. Come per qualsiasi elemento standard, possono essere dichiarati in HTML o creati in DOM mediante 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);

Creare un'istanza per gli elementi di estensione dei tipi

La creazione di un'istanza per elementi personalizzati in stile estensione del tipo è 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, esiste una versione sovraccarica 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 la presenza di un nuovo tag, ma non è molto utile. 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. Puoi considerarlo 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);

Ovviamente esistono mille modi per costruire un prototype. Se non ti piace creare prototipi come questo, ecco una versione più condensata 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 di utilizzare get/set.

Metodi di callback del ciclo di vita

Gli elementi possono definire metodi speciali per sfruttare momenti interessanti della loro esistenza. Questi metodi sono denominati in modo appropriato i callback del ciclo di vita. Ognuno di questi 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 listener di eventi predefiniti nell'elemento:

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

Aggiunta del markup

Abbiamo creato <x-foo>, ma è vuoto. Dovremmo fornire del codice HTML per il rendering?

In questo caso, i callback del ciclo di vita sono molto utili. In particolare, possiamo usare createdCallback() per dotare un elemento di codice 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});

La creazione di un'istanza per questo tag e l'ispezione in DevTools (fai clic con il tasto destro del mouse e seleziona Ispeziona elemento) dovrebbero mostrare:

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

Incapsulamento degli elementi interni in shadow DOM

Shadow DOM da solo è un potente strumento per incapsulare i contenuti. Usala insieme a elementi personalizzati e diventa magico.

Shadow DOM offre agli elementi personalizzati:

  1. Un modo per nascondere le viscere, proteggendo così gli utenti dai dettagli cruenti dell'implementazione.
  2. Stile incapsulamento... senza costi.

Creare un elemento da Shadow DOM è come crearne uno che renda il markup di base. La differenza è per 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 una radice shadow per <x-foo-shadowdom> e l'ho riempita con markup. Con l'impostazione "Mostra DOM shadow" abilitata in DevTools, viene visualizzata una #shadow-root che può essere espansa:

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

Quella è la Radice delle Ombre!

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 DOM <template> e Shadow:

<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. Cerchiamo di capire cosa sta succedendo:

  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 spaventosi 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!

Aggiungere stili agli elementi personalizzati

Come per qualsiasi tag HTML, gli utenti del tuo tag personalizzato possono definire lo stile con 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>

Applicare stili agli elementi che utilizzano lo shadow DOM

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

Shadow DOM infonde un elemento con l'incapsulamento dello stile. 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 del DOM delle ombre sono un argomento importante. Per saperne di più, ti consiglio di leggere 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 elementi non risolti, fino al punto in cui il browser richiama il tuo createdCallback() (vedi i metodi del ciclo di vita). A questo punto, l'elemento non è più irrisolto. Il processo di upgrade è stato completato e l'elemento è stato trasformato nella sua 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 delle caratteristiche

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 atterrato dietro a una bandiera in Chrome 27 e Firefox ~23. Tuttavia, la specifica è molto cambiata da allora. Chrome 31 è il primo a offrire un supporto reale per le specifiche aggiornate.

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

Che cosa è successo a HTMLElementElement?

Coloro che hanno seguito il lavoro di standardizzazione hanno ricevuto una sola volta <element>. Erano le ginocchia delle api. 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'apocalisse 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 servizio, 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 che ho descritto nella sezione Creare elementi da un modello.

Conclusione

Gli elementi personalizzati ci forniscono lo strumento per estendere il vocabolario dell'HTML, insegnargli nuovi trucchi e saltare attraverso i wormhole della piattaforma web. Combinali con altre nuove primitive della piattaforma come Shadow DOM e <template> per iniziare a comprendere il quadro 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.