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. Pourtant, c'est ainsi que nous créons des applications Web. C'est triste.
Ne devrions-nous pas exiger plus de notre plate-forme ?
Balisage sexy. Faisons-en une réalité
Le langage HTML est un excellent outil pour structurer un document, mais son vocabulaire est limité aux éléments définis par la norme HTML.
Que se passerait-il si le balisage de Gmail n'était pas atroce ? 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. Il est pertinent, facile à comprendre et, surtout, facile à gérer. Mon moi futur/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 des composants Web, 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
- Regrouper de manière 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 la 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 le prototype
de l'élément.
C'est là que vous pouvez ajouter des fonctionnalités personnalisées (par exemple, des propriétés et des 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 est équivalent à:
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.
Éléments étendus
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 en faire un "super bouton". Pour étendre l'élément <button>
, créez un élément qui hérite du prototype
de HTMLButtonElement
et du extends
du nom de l'élément. Dans ce cas, "button":
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 Ajouter des propriétés et des méthodes JavaScript ci-dessous.
Comment les éléments sont-ils mis à niveau ?
Vous êtes-vous déjà demandé pourquoi l'analyseur HTML ne génère pas d'erreur pour les balises non standards ?
Par exemple, il est tout à fait acceptable de déclarer <randomtag>
sur la page. Selon la spécification HTML:
Désolé, <randomtag>
. Vous n'êtes pas standard et 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
. Vous pouvez vérifier cela en démarrant la console: Ctrl + Shift + J
(ou Cmd + Opt + J
sur Mac), puis en collant 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 que les éléments ne soient mis à niveau 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> |
Instancier des é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 dans le 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 est très 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 constater, 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 indiquer au navigateur une nouvelle balise, mais cela ne sert pas à grand-chose. 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 sur la définition de l'élément. Il s'agit d'une façon 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 appelées rappels de cycle de vie. Chacun 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éfinition de 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/lorsque cela est pertinent.
Par exemple, supposons que votre élément soit suffisamment complexe et qu'il ouvre une connexion à IndexedDB dans createdCallback()
. Avant qu'il ne soit supprimé du DOM, effectuez le nettoyage nécessaire dans detachedCallback()
. Remarque:Vous ne devez pas vous fier à cela, par exemple, si l'utilisateur ferme l'onglet, mais considérez-le comme un crochet d'optimisation possible.
Un autre cas d'utilisation des rappels de cycle de vie consiste à configurer des écouteurs d'événements par défaut sur l'élément:
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
Ajouter un 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 les éléments suivants:
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Encapsulation des éléments internes dans Shadow DOM
Le Shadow DOM est un outil puissant pour encapsuler du contenu. Utilisez-le avec des éléments personnalisés pour créer des effets magiques.
Le Shadow DOM offre aux éléments personnalisés les avantages suivants:
- Un moyen de cacher ses entrailles, et donc de protéger les utilisateurs des détails d'implémentation sanglants.
- Encapsulation de style… 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 le .innerHTML
de l'élément, j'ai créé une racine d'ombre pour <x-foo-shadowdom>
, puis je l'ai remplie de balisage.
Lorsque le paramètre "Afficher le Shadow DOM" est activé dans les outils pour les développeurs, un #shadow-root
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 du 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 efficaces. 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 du Shadow DOM
- Le Shadow DOM permet d'encapsuler le style de l'élément (par exemple,
p {color: orange;}
ne rend pas l'ensemble de la page orange).
C'est parfait !
Mettre en forme des é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>
Mettre en forme des é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 Shadow DOM héritent de ses nombreux avantages.
Le Shadow DOM insuffle à un élément une encapsulation de style. Les styles définis dans un nœud racine d'ombre ne fuient 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 de style permettent également aux éléments personnalisés de définir des styles par défaut pour eux-mêmes.
Le style Shadow DOM est un sujet vaste. 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 et stylisation.
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 point où le navigateur appelle votre createdCallback()
(voir les méthodes de cycle de vie).
L'élément n'est alors plus considéré comme non résolu. Le processus de migration est terminé et l'élément s'est transformé en sa définition.
Exemple: afficher les balises "x-foo" en fondu lorsqu'elles sont enregistrées:
<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>
Historique et compatibilité avec les navigateurs
Détection de fonctionnalités
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é à s'afficher derrière un indicateur dans Chrome 27 et Firefox 23. Toutefois, la spécification a beaucoup évolué depuis. Chrome 31 est la première version à prendre en charge la spécification mise à jour.
En attendant que la compatibilité des navigateurs soit 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 le top. Vous pouvez l'utiliser pour enregistrer de manière déclarative de nouveaux éléments:
<element name="my-element">
...
</element>
Malheureusement, le processus de migration a été confronté à trop de problèmes de synchronisation, de cas particuliers et de scénarios apocalyptiques pour que nous puissions tout résoudre. <element>
a dû être mis de côté. En août 2013, Dimitri Glazkov a annoncé sur public-webapps la suppression de cette fonctionnalité, 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 Créer des éléments à partir d'un modèle.
Conclusion
Les éléments personnalisés nous donnent l'outil pour étendre le vocabulaire HTML, lui apprendre de nouveaux trucs et passer par les trous de ver de la plate-forme Web. En les combinant aux autres nouvelles primitives de plate-forme telles que Shadow DOM et <template>
, nous commençons à cerner le tableau des Web Components. Le balisage 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.