Utilizzare gli elementi personalizzati

Boris Smus
Boris Smus

Introduzione

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

Gmail

Non c'è niente di moderno nella zuppa <div>. Eppure, è così che creiamo app web. È triste. Non dovremmo chiedere di più alla nostra piattaforma?

Sexy markup. 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 di Gmail non fosse atroce? 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 rinfrescante! Anche quest'app ha senso. È significativo, facile da capire e soprattutto è sostenibile. Dal futuro io/lo saprete esattamente cosa fa solo esaminando la sua spina dorsale dichiarativa.

Per iniziare

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

  1. Definizione di 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 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 restrizione consente al parser di distinguere gli elementi personalizzati da quelli regolari, ma garantisce anche la compatibilità in avanti quando nuovi tag vengono aggiunti 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. Scopri di più più avanti.

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

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

Una chiamata a document.registerElement('x-foo') informa il browser del nuovo elemento e restituisce un costruttore da 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.

Estensione di elementi

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

Estensione di elementi nativi

Supponiamo che tu non sia soddisfatto del regolare Joe <button>. Vorresti potenziare al massimo le sue funzionalità. 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 dagli elementi nativi sono chiamati elementi personalizzati di estensione del tipo. Ereditano da una versione specializzata di HTMLElement per dire "l'elemento X è una Y".

Esempio:

<button is="mega-button">

Estensione di un elemento personalizzato

Per creare un elemento <x-foo-extended> che estende l'elemento personalizzato <x-foo>, è sufficiente ereditare il prototipo e indicare da quale tag stai ereditando:

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

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

Consulta la sezione Aggiunta di proprietà e metodi JS di seguito per ulteriori informazioni sulla creazione di prototipi di elementi.

Come viene eseguito l'upgrade degli elementi

Si è mai chiesto perché l'analizzatore sintattico HTML non restituisce un valore per i tag non standard? Ad esempio, è assolutamente felice se dichiariamo <randomtag> nella pagina. In base alla specifica HTML:

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

Lo stesso non vale per gli elementi personalizzati. Gli elementi con nomi di elementi personalizzati validi ereditano da HTMLElement. Puoi verificarlo attivando la console: Ctrl + Shift + J (o Cmd + Opt + J su Mac) e incollando le seguenti righe di codice; verrà restituito 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 tramite script utilizzando document.registerElement(), possono essere dichiarati o creati prima della registrazione della relativa definizione dal browser. Ad esempio, puoi dichiarare <x-tabs> nella pagina, ma finire per richiamare document.registerElement('x-tabs') molto più tardi.

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

Questa tabella potrebbe aiutarti a capire come funziona:

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 ancora agli elementi personalizzati. Come per qualsiasi elemento standard, possono essere dichiarati in HTML o creati in DOM utilizzando JavaScript.

Creazione di istanze 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 degli elementi di estensione del tipo

La creazione di istanze di elementi personalizzati in stile 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 utilizza l'attributo is="" come secondo parametro.

Utilizza l'operatore new:

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

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

Aggiunta di proprietà e metodi JS

La cosa più importante degli elementi personalizzati è che puoi raggruppare funzionalità su misura 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);

Ovviamente ci sono mille modi per costruire un prototype. Se non ti piace creare prototipi come questo, ecco una versione più condensata dello stesso aspetto:

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 di utilizzare 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 attingere a momenti interessanti della loro esistenza. Questi metodi vengono denominati callback del ciclo di vita in modo appropriato. Ognuno di essi ha un nome e uno scopo specifici:

Nome 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 ha senso. Ad esempio, supponiamo che il tuo elemento sia sufficientemente complesso e che 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 considerala come un possibile hook di ottimizzazione.

Un altro caso d'uso per i callback del ciclo di vita è configurare i listener di eventi predefiniti sull'elemento:

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

Aggiunta del markup

Abbiamo creato <x-foo> e le abbiamo fornito un'API JavaScript, ma è vuoto. Daremo del codice HTML per il rendering?

In questo caso, i callback del ciclo di vita sono utili. In particolare, possiamo utilizzare 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 dell'istanza di 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

Di per sé, Shadow DOM è un potente strumento per l'incapsulamento dei contenuti. Usala in combinazione con elementi personalizzati e le cose diventano magiche!

Il DOM shadow fornisce elementi personalizzati:

  1. Un modo per nasconderlo, proteggendo così gli utenti da dettagli di implementazione cruenti.
  2. Incapsulamento dello stile... senza costi.

La creazione di un elemento da Shadow DOM è come la creazione di un elemento che esegue il rendering del 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});

Anziché impostare il valore .innerHTML dell'elemento, ho creato una radice ombra per <x-foo-shadowdom> e l'ho riempita con il markup. Con l'impostazione"Mostra shadow DOM" in DevTools, vedrai un elemento #shadow-root che può essere espanso:

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

Questa è la Radice Ombra!

Creazione di elementi da un modello

I modelli HTML sono un'altra nuova API primitiva che si adatta perfettamente al 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 hanno un impatto notevole. Cerchiamo di capire tutto ciò che 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. Il DOM shadow fornisce l'incapsulamento dello stile dell'elemento (ad esempio, p {color: orange;} non diventa arancione l'intera pagina)

Molto bene!

Stile degli elementi personalizzati

Come per qualsiasi tag HTML, gli utenti del tag personalizzato possono utilizzare 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>

Stili degli elementi che utilizzano Shadow DOM

La tana del coniglio va molto molto più in profondità quando aggiungi Shadow DOM al mix. Gli elementi personalizzati che utilizzano Shadow DOM ne ereditano i grandi vantaggi.

Shadow DOM infonde un elemento con l'incapsulamento di stile. Gli stili definiti in una radice scura non escono dall'host e non svaniscono dalla pagina. Nel caso di un elemento personalizzato, l'elemento è l'host. Le proprietà dell'incapsulamento degli stili consentono inoltre agli elementi personalizzati di definire autonomamente gli stili predefiniti.

Lo stile DOM delle ombre è un argomento enorme. Per saperne di più, ti consiglio di leggere altri miei articoli:

Prevenzione del FOUC mediante :unresolved

Per ridurre il valore di 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 chiama createdCallback() (vedi i metodi del ciclo di vita). Quando ciò accade, l'elemento non è più un elemento irrisolto. Il processo di upgrade è completo e l'elemento si è trasformato nella sua definizione.

Esempio: applica la dissolvenza nei tag "x-foo" quando sono 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 (consulta la sezione 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>

Supporto della cronologia e del browser

Rilevamento delle funzionalità

Il rilevamento della caratteristica deve essere controllato se document.registerElement() esiste:

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

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

Supporto del browser

document.registerElement() ha iniziato a essere visualizzato dietro una bandiera in Chrome 27 e Firefox 23 circa. Tuttavia, le specifiche si sono evolute molto da allora. Chrome 31 è il primo ad avere un supporto reale per le specifiche aggiornate.

Fino a quando il supporto dei browser non sarà ottimale, c'è il polyfill, che viene utilizzato da Polymer di Google e da X-Tag di Mozilla.

Che cosa è successo a HTMLElementElement?

Coloro che hanno seguito il lavoro di standardizzazione, sai che c'era una volta <element>. Erano le ginocchia delle api. Puoi utilizzarlo per registrare in modo dichiarativo nuovi elementi:

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

Purtroppo, per risolvere il problema si sono verificati troppi problemi di tempistica nella procedura di upgrade, nelle richieste angolari e negli scenari simili ad Armageddon. <element> ha dovuto essere accantonato. Nell'agosto 2013, Dimitri Glazkov ha pubblicato un post su app-web-pubbliche annunciandone la rimozione, 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 Creazione di elementi da un modello.

Conclusione

Gli elementi personalizzati ci danno lo strumento per estendere il vocabolario dell'HTML, insegnare nuovi trucchi e sfrecciare attraverso i wormhole della piattaforma web. Combinandole con altre nuove primitive di piattaforma come Shadow DOM e <template>, iniziamo a realizzare il quadro dei componenti web. Il markup può essere di nuovo sexy.

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