Introduzione
Il web è totalmente privo di espressione. Per capire cosa intendo, dai un'occhiata a un'app web "moderna" come 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:
- Definizione di nuovi elementi HTML/DOM
- Creare elementi che si estendono da altri elementi
- Raggruppare logicamente le funzionalità personalizzate in un unico tag
- 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:
- Un modo per nasconderlo, proteggendo così gli utenti da dettagli di implementazione cruenti.
- 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>.
</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>.
</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:
- Abbiamo registrato un nuovo elemento in HTML:
<x-foo-from-template>
- Il DOM dell'elemento è stato creato da un
<template>
- I dettagli spaventosi dell'elemento vengono nascosti utilizzando Shadow DOM
- 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:
- "A Guide to Styling Elements" (Guida agli elementi di stile) nella documentazione di Polymer.
- "Shadow DOM 201: CSS & Styling" qui.
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.