Introduction
Le Web manque cruellement d'expression. Pour comprendre ce que je veux dire, jetez un œil à une application Web "moderne" comme Gmail :
La soupe <div>
n'a rien de moderne. Et pourtant, c'est ainsi que nous
concevons des applications web. C'est triste.
Ne devrions-nous pas exiger plus de notre plate-forme ?
Balisage sexy. C'est parti
HTML constitue un excellent outil pour structurer un document, mais son vocabulaire se limite aux éléments définis par la norme HTML.
Que se passe-t-il si le balisage pour Gmail n'est pas trop agressif ? Et si c'était beau ?
<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>
Quelle fraîcheur ! Cette application est tout à fait logique. Elle est pertinente, facile à comprendre et, surtout, souhaitable. Vous saurez exactement ce qu'il fait simplement en examinant sa colonne vertébrale déclarative.
Premiers pas
Les éléments personnalisés permettent aux développeurs Web de définir de nouveaux types d'éléments HTML. La spécification fait partie de plusieurs nouvelles primitives d'API qui s'inscrivent sous le parapluie Web Components, mais elle est probablement la plus importante. Les composants Web n'existent pas sans les fonctionnalités débloquées par les éléments personnalisés :
- Définir de nouveaux éléments HTML/DOM
- Créer des éléments qui s'étendent à partir d'autres éléments
- Regroupement logique des fonctionnalités personnalisées dans une seule balise
- Étendre l'API des éléments DOM existants
Enregistrer de nouveaux éléments
Les éléments personnalisés sont créés à l'aide de document.registerElement()
:
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
Le premier argument de document.registerElement()
est le nom de balise de l'élément.
Le nom doit contenir un tiret (-). Par exemple, <x-tags>
, <my-element>
et <my-awesome-app>
sont tous des noms valides, tandis que <tabs>
et <foo_bar>
ne le sont pas. Cette restriction permet à l'analyseur de distinguer les éléments personnalisés des éléments standards, mais assure également la compatibilité ascendante lorsque de nouvelles balises sont ajoutées au code HTML.
Le deuxième argument est un objet (facultatif) décrivant l'élément prototype
de l'élément.
C'est ici que vous pouvez ajouter des fonctionnalités personnalisées (par exemple, des propriétés et méthodes publiques) à vos éléments.
Nous y reviendrons plus tard.
Par défaut, les éléments personnalisés héritent de HTMLElement
. Par conséquent, l'exemple précédent équivaut à :
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
Un appel à document.registerElement('x-foo')
informe le navigateur du nouvel élément et renvoie un constructeur que vous pouvez utiliser pour créer des instances de <x-foo>
.
Vous pouvez également utiliser les autres techniques d'instanciation d'éléments si vous ne souhaitez pas utiliser le constructeur.
Extension des éléments
Les éléments personnalisés vous permettent d'étendre les éléments HTML existants (natifs) ainsi que d'autres éléments personnalisés. Pour étendre un élément, vous devez transmettre à registerElement()
le nom et le prototype
de l'élément à hériter.
Étendre les éléments natifs
Disons que vous n'êtes pas satisfait de l'abonnement Ordinaire Joe <button>
. Vous souhaitez renforcer ses fonctionnalités pour en faire un "super bouton". Pour étendre l'élément <button>
, créez un élément qui hérite du prototype
de HTMLButtonElement
et extends
du nom de l'élément. Dans ce cas, « bouton » :
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
Les éléments personnalisés qui héritent d'éléments natifs sont appelés éléments personnalisés d'extension de type.
Ils héritent d'une version spécialisée de HTMLElement
pour indiquer "l'élément X est un Y".
Exemple :
<button is="mega-button">
Étendre un élément personnalisé
Pour créer un élément <x-foo-extended>
qui étend l'élément personnalisé <x-foo>
, il vous suffit d'hériter de son prototype et d'indiquer la balise dont vous héritez :
var XFooProto = Object.create(HTMLElement.prototype);
...
var XFooExtended = document.registerElement('x-foo-extended', {
prototype: XFooProto,
extends: 'x-foo'
});
Pour en savoir plus sur la création de prototypes d'éléments, consultez la section Ajouter des propriétés et des méthodes JS ci-dessous.
Comment les éléments sont-ils mis à niveau ?
Vous êtes-vous déjà demandé pourquoi l'analyseur HTML ne corrige pas les balises non standards ?
Par exemple, il est tout à fait possible de déclarer <randomtag>
sur la page. Selon la spécification HTML :
Désolé, <randomtag>
. Vous n'êtes pas standard et vous héritez de HTMLUnknownElement
.
Il n'en va pas de même pour les éléments personnalisés. Les éléments dont les noms d'éléments personnalisés sont valides héritent de HTMLElement
. Pour le vérifier, lancez la console Ctrl + Shift + J
(ou Cmd + Opt + J
sur Mac) et collez les lignes de code suivantes. Elles renvoient 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
Éléments non résolus
Étant donné que les éléments personnalisés sont enregistrés par script à l'aide de document.registerElement()
, ils peuvent être déclarés ou créés avant que leur définition ne soit enregistrée par le navigateur. Par exemple, vous pouvez déclarer <x-tabs>
sur la page, mais finir par appeler document.registerElement('x-tabs')
beaucoup plus tard.
Avant la mise à niveau des éléments vers leur définition, ils sont appelés éléments non résolus. Il s'agit d'éléments HTML dont le nom d'élément personnalisé est valide, mais qui n'ont pas été enregistrés.
Ce tableau peut vous aider à y voir plus clair :
Nom | Hérite de | Exemples |
---|---|---|
Élément non résolu | HTMLElement |
<x-tabs> , <my-element> |
Élément inconnu | HTMLUnknownElement |
<tabs> , <foo_bar> |
Instanciation d'éléments
Les techniques courantes de création d'éléments s'appliquent toujours aux éléments personnalisés. Comme pour tout élément standard, ils peuvent être déclarés en HTML ou créés en DOM à l'aide de JavaScript.
Instancier des balises personnalisées
Déclarez-les:
<x-foo></x-foo>
Créer un DOM en JS :
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('Thanks!');
});
Utilisez l'opérateur new
:
var xFoo = new XFoo();
document.body.appendChild(xFoo);
Instancier des éléments d'extension de type
L'instanciation d'éléments personnalisés de type extension de type est étonnamment proche des balises personnalisées.
Déclarez-les:
<!-- <button> "is a" mega button -->
<button is="mega-button">
Créer un DOM en JS :
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
Comme vous pouvez le voir, il existe désormais une version surchargée de document.createElement()
qui utilise l'attribut is=""
comme deuxième paramètre.
Utilisez l'opérateur new
:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
Jusqu'à présent, nous avons appris à utiliser document.registerElement()
pour signaler une nouvelle balise au navigateur, mais son utilisation n'est pas très utile. Ajoutons des propriétés et des méthodes.
Ajouter des propriétés et des méthodes JavaScript
L'avantage des éléments personnalisés est que vous pouvez regrouper des fonctionnalités personnalisées avec l'élément en définissant des propriétés et des méthodes dans la définition de l'élément. Il s'agit d'un moyen de créer une API publique pour votre élément.
Voici un exemple complet:
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);
Bien sûr, il existe des milliers de façons de construire un prototype
. Si vous n'aimez pas créer de prototypes comme celui-ci, voici une version plus condensée de la même chose :
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype, {
bar: {
get: function () {
return 5;
}
},
foo: {
value: function () {
alert('foo() called');
}
}
})
});
Le premier format permet d'utiliser Object.defineProperty
ES5. La seconde permet d'utiliser get/set.
Méthodes de rappel de cycle de vie
Les éléments peuvent définir des méthodes spéciales pour exploiter des moments intéressants de leur existence. Ces méthodes sont nommées de manière appropriée comme rappels de cycle de vie. Chacune d'elles a un nom et un objectif spécifiques:
Nom du rappel | Appelé lorsque |
---|---|
createdCallback | une instance de l'élément est créée |
attachedCallback | une instance a été insérée dans le document ; |
detachedCallback | une instance a été supprimée du document ; |
attributeChangedCallback(attrName, oldVal, newVal) | un attribut a été ajouté, supprimé ou mis à jour ; |
Exemple:définir createdCallback()
et attachedCallback()
sur <x-foo>
:
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};
var XFoo = document.registerElement('x-foo', {prototype: proto});
Tous les rappels de cycle de vie sont facultatifs, mais définissez-les si nécessaire.
Par exemple, supposons que votre élément soit suffisamment complexe et qu'il ouvre une connexion à IndexedDB dans createdCallback()
. Avant de le supprimer du DOM, effectuez le nettoyage nécessaire dans detachedCallback()
. Remarque : Vous ne devez pas vous y fier, par exemple, si l'utilisateur ferme l'onglet, mais considérez-le comme un crochet d'optimisation possible.
Les rappels de cycle de vie sont également utilisés pour configurer des écouteurs d'événements par défaut sur l'élément :
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
Ajouter du balisage
Nous avons créé <x-foo>
, lui avons attribué une API JavaScript, mais elle est vide. Voulez-vous lui donner du code HTML à afficher ?
Les rappels de cycle de vie sont utiles ici. Plus précisément, nous pouvons utiliser createdCallback()
pour doter un élément d'un code HTML par défaut :
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'instanciation de cette balise et son inspection dans les outils de développement (clic droit, sélectionnez "Inspecter l'élément") devraient afficher :
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Encapsulation des composants internes dans Shadow DOM
À lui seul, le Shadow DOM est un outil puissant qui permet d'encapsuler du contenu. Utilisez-le avec des éléments personnalisés pour créer des effets magiques.
Shadow DOM fournit des éléments personnalisés:
- Un moyen de cacher leur intuition afin de protéger les utilisateurs des détails d'implémentation sanglants.
- L'encapsulation des styles... sans frais.
Créer un élément à partir du Shadow DOM revient à en créer un qui affiche une balise de base. La différence se trouve dans 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});
Au lieu de définir la .innerHTML
de l'élément, j'ai créé une racine fantôme pour <x-foo-shadowdom>
, puis j'y ai ajouté un balisage.
Lorsque le paramètre "Afficher le Shadow DOM" est activé dans les outils de développement, un #shadow-root
s'affiche et peut être développé:
▾<x-foo-shadowdom>
▾#shadow-root
**I'm in the element's Shadow DOM!**
</x-foo-shadowdom>
C'est la racine d'ombre.
Créer des éléments à partir d'un modèle
Les modèles HTML sont une autre nouvelle primitive d'API qui s'intègre parfaitement au monde des éléments personnalisés.
Exemple : enregistrement d'un élément créé à partir d'un <template>
et d'un Shadow DOM :
<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>
Ces quelques lignes de code sont très percutantes. Voyons ce qui se passe :
- Nous avons enregistré un nouvel élément en HTML :
<x-foo-from-template>
- Le DOM de l'élément a été créé à partir d'un
<template>
- Les détails effrayants de l'élément sont masqués à l'aide de Shadow DOM.
- Shadow DOM fournit l'encapsulation du style de l'élément (par exemple,
p {color: orange;}
ne rend pas l'ensemble de la page orange).
Trop bien !
Appliquer un style aux éléments personnalisés
Comme pour toute balise HTML, les utilisateurs de votre balise personnalisée peuvent lui appliquer un style à l'aide de sélecteurs :
<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>
Appliquer un style aux éléments qui utilisent Shadow DOM
Le terrier du lapin est beaucoup plus profond lorsque vous ajoutez le Shadow DOM. Les éléments personnalisés qui utilisent le Shadow DOM héritent de ses avantages.
Le Shadow DOM insuffle à un élément une encapsulation de style. Les styles définis dans un nœud racine d'ombre ne s'échappent pas de l'hôte et ne s'infiltrent pas depuis la page. Dans le cas d'un élément personnalisé, l'élément lui-même est l'hôte. Les propriétés de l'encapsulation du style permettent également aux éléments personnalisés de définir eux-mêmes des styles par défaut.
Les styles Shadow DOM sont un vaste sujet. Pour en savoir plus, je vous recommande de consulter quelques-uns de mes autres articles :
- Guide de stylisation des éléments dans la documentation de Polymer.
- "Shadow DOM 201: CSS & Styling" ici.
Prévention des erreurs de redirection à l'aide de :unresolved
Pour atténuer les FOUC, les éléments personnalisés spécifient une nouvelle pseudo-classe CSS, :unresolved
. Utilisez-le pour cibler les éléments non résolus, jusqu'au moment où le navigateur appelle votre createdCallback()
(voir Méthodes de cycle de vie).
Une fois que cela se produit, l'élément n'est plus un élément non résolu. Le processus de mise à niveau est terminé et l'élément a été transformé en définition.
Exemple: Ajout de tags "x-foo" en fondu au moment de leur enregistrement:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
N'oubliez pas que :unresolved
ne s'applique qu'aux éléments non résolus, et non aux éléments qui héritent de HTMLUnknownElement
(voir Comment les éléments sont mis à niveau).
<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>
Prise en charge de l'historique et des navigateurs
Détection de caractéristiques
La détection de fonctionnalités consiste à vérifier si document.registerElement()
existe :
function supportsCustomElements() {
return 'registerElement' in document;
}
if (supportsCustomElements()) {
// Good to go!
} else {
// Use other libraries to create components.
}
Prise en charge des navigateurs
document.registerElement()
a commencé à atterrir derrière un indicateur dans Chrome 27 et Firefox ~23. Cependant, la spécification a beaucoup évolué depuis. Chrome 31 est le premier à être
vraiment compatible avec la spécification mise à jour.
Tant que la compatibilité des navigateurs n'est pas optimale, un polyfill est utilisé par Polymer de Google et par la balise X de Mozilla.
Qu'est-il arrivé à HTMLElementElement ?
Pour ceux qui ont suivi le travail de normalisation, vous savez qu'il y avait autrefois <element>
.
C'était les genoux d'abeilles. Vous pouvez l'utiliser pour enregistrer de nouveaux éléments de manière déclarative:
<element name="my-element">
...
</element>
Malheureusement, le processus de mise à niveau, les cas rectangulaires et les scénarios de type Armageddon étaient trop nombreux pour nous permettre de tout résoudre. <element>
a dû être placé sur les étagères. En août 2013, Dimitri Glazkov a publié sur public-webapps un message annonçant son retrait, du moins pour le moment.
Notez que Polymer implémente une forme déclarative d'enregistrement d'éléments avec <polymer-element>
. Comment ? Il utilise document.registerElement('polymer-element')
et les techniques décrites dans la section Créer des éléments à partir d'un modèle.
Conclusion
Les éléments personnalisés nous permettent d'étendre le vocabulaire HTML, de lui enseigner de nouvelles astuces et de nous orienter dans les moindres détails de la plate-forme Web. Combinez-les aux autres nouvelles primitives de la plate-forme, telles que Shadow DOM et <template>
, et nous commencerons à mieux comprendre les composants Web. La balise peut à nouveau être sexy !
Si vous souhaitez vous lancer avec les composants Web, je vous recommande de consulter Polymer. Il est plus que suffisant pour vous lancer.