I. Introduction▲
Le Web manque cruellement d'expressivité. Pour comprendre ce que j'entends par là, prenez une application Web « moderne » comme GMail :
Il n'y a rien de moderne avec une telle soupe de balises <div>. Pourtant… c'est actuellement comme ça que nous construisons ce type d'applications. C'est malheureux. Ne devrions-nous pas attendre mieux de nos plateformes ?
I-A. Rendre possible un balisage agréable▲
HTML nous offre un excellent outil pour structurer un document, mais son vocabulaire est limité aux éléments définis dans les spécifications HTML.
À quoi ressemblerait GMail si son balisage n'était pas horrible ? Comment pourrait-on le rendre plus 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>
<p>Heard of it?</p>
</hangout-message>
</hangout-discussion>
</hangout-chat>
<hangout-chat>...</hangout-chat>
</hangout-module>
C'est beaucoup plus élégant ! Surtout, cette application a du sens. Elle est éloquente, facile à comprendre et surtout, maintenable. N'importe quel utilisateur futur saura exactement ce qu'elle fait juste en regardant son ossature.
Allez les éléments personnalisés, aidez-nous ! Vous êtes notre seul espoir.
II. Pour démarrer▲
L'API Custom Elements permet aux développeurs Web de définir de nouveaux types d'éléments HTML. La spécification est l'une des nouvelles API majeures apparues sous la bannière des composants Web (Web Components), mais elle est surtout probablement la plus importante d'entre elles. D'ailleurs, les composants Web ne peuvent pas exister sans les fonctionnalités apportées par les éléments personnalisés :
- définir de nouveaux éléments HTML/DOM ;
- créer des éléments qui en étendent d'autres ;
- regrouper dans une balise unique des fonctionnalités liées ;
- étendre l'API d'éléments DOM existants.
II-A. Enregistrer de nouveaux éléments▲
Les éléments personnalisés sont créés avec document.register() :
Le premier paramètre de document.register() correspond au nom de la balise créée. Ce nom doit obligatoirement contenir un tiret (-). Par exemple, <x-tags>, <my-element> et <my-awesome-app> sont des noms valides, alors que <tabs> et <foo_bar> ne le sont pas. Cette restriction permet au parseur de différencier les éléments personnalisés des éléments natifs, mais permet surtout d'assurer une compatibilité ascendante lorsque de nouvelles balises sont ajoutées au HTML.
Le second paramètre (optionnel) est un objet décrivant le prototype de l'élément. C'est ici qu'il faudra définir les fonctionnalités (par exemple ses propriétés et méthodes publiques) de vos éléments. Nous y reviendronsAjouter des propriétés et des méthodes JavaScript.
Par défaut, les nouveaux éléments héritent de l'objet HTMLElement. Ainsi, l'exemple précédent est équivalent à :
var XFoo =
document
.register
(
'x-foo'
,
{
prototype
:
Object.create
(
HTMLElement.
prototype)
}
);
Un appel à document.register('x-foo') permet au navigateur de reconnaître le nouvel élément et retourne un constructeur que vous pouvez utiliser pour créer des instances de <x-foo>. Vous pouvez également utiliser d'autres techniques pour instancier des élémentsInstancier les éléments si vous ne souhaitez pas utiliser le constructeur.
Astuce
Si vous ne souhaitez pas que le constructeur soit dans le contexte global, placez-le dans un espace de nommage : var myapp = {}; myapp.XFoo = document.register('x-foo');
II-B. Étendre des éléments natifs▲
Imaginons que vous ne soyez pas particulièrement satisfait de la balise <button>. En fait, vous aimeriez l'enrichir pour la faire devenir un « mega bouton ». Pour étendre l'élément <button>, il suffit de créer un élément personnalisé qui hérite du prototype de HTMLButtonElement :
var MegaButton =
document
.register
(
'mega-button'
,
{
prototype
:
Object.create
(
HTMLButtonElement.
prototype)
}
);
À savoir
Pour créer un élément A qui étende un élément B, l'élément A doit hériter du prototype de l'élément B.
Cette catégorie d'éléments personnalisés s'appelle « élément personnalisé par extension de type » (type extension custom elements). Ils héritent d'une version spécifique de HTMLElement comme si on disait « l'élément X est un Y ».
Exemple :
<button is
=
"mega-button"
>
II-C. Comment sont gérés les éléments▲
Ne vous êtes-vous jamais demandé pourquoi les parseurs HTML des navigateurs ne sont pas perturbés lorsqu'ils rencontrent des éléments non standards ? Par exemple, vous pouvez mettre une balise <randomtag> dans votre code et le navigateur ne s'en portera pas plus mal. En fait, la spécification HTML prévoit ce cas :
The HTMLUnknownElement interface must be used for HTML elements that are not defined by this specification.
Ce qui peut se traduire par :
L'interface HTMLUnknownElement doit être utilisée pour les éléments HTML non définis par cette spécification.
Désolé <randomtag>, tu es inconnu des standards et hériteras de HTMLUnknownElement.
Mais cela ne s'applique pas aux éléments personnalisés. Les éléments personnalisés ayant un nom valide héritent de HTMLElement. Vous pouvez vérifier cela en activant votre console JavaScript et en exécutant le code ci-après, les exemples renvoient true :
// "tabs" n'est pas un nom d'élément personnalisé valide
document
.createElement
(
'tabs'
).
__proto__ ===
HTMLUnknownElement.
prototype
// "x-tabs" est un nom d'élément personnalisé valide
document
.createElement
(
'x-tabs'
).
__proto__ ==
HTMLElement.
prototype
Note
<x-tabs> sera malgré tout de type HTMLUnknownElement pour les navigateurs ne supportant pas document.register().
II-C-1. Éléments non résolus▲
Comme les nouveaux éléments sont signifiés au navigateur par script via document.register(), il se peut qu'ils soient utilisés avant que leur définition ne soit interprétée. Par exemple, vous pouvez utiliser une balise <x-tab> dans le HTML, mais n'appeler document.register('x-tabs') que plus tard.
Avant que les éléments ne soient rattachés à leur définition, ils sont dits « non résolus » (unresolved elements). Il s'agit des éléments ayant un nom valide, mais n'ayant pas encore été enregistrés.
Voici un tableau qui vous aidera à y voir plus clair.
Nom |
Hérite de |
Exemples |
Élément non résolu |
HTMLElement |
<x-tabs>, <my-element>, <my-awesome-app> |
Élément inconnu |
HTMLUnknownElement |
<tabs>, <foo_bar> |
On peut assimiler les éléments non résolus à des oublis. Ils sont potentiellement éligibles pour être validés par le navigateur ultérieurement. Le navigateur estime qu'ils ont toutes les propriétés requises pour être de nouveaux éléments et attend leur définition pour les valider.
III. Instancier les éléments▲
La méthode habituelle pour créer des éléments s'applique aussi aux éléments personnalisés. Comme tout élément standard, ils peuvent être présents dans le HTML ou créés dans le DOM à l'aide de JavaScript.
III-A. Instancier de nouvelles balises▲
Déclarez-les :
<x-foo></x-foo><x-foo></x-foo>
Créez un objet DOM en JavaScript :
var xFoo =
document
.createElement
(
'x-foo'
);
xFoo.addEventListener
(
'click'
,
function(
e) {
alert
(
'Thanks!'
);
}
);
Utilisez l'opérateur new :
III-B. Instancier des éléments par extension de type▲
Instancier des extensions d'éléments existants se fait de façon similaire.
Déclarez-les :
<!-- <button> "est un" mega bouton -->
<button is
=
"mega-button"
>
Créez un objet DOM en JavaScript :
var megaButton =
document
.createElement
(
'button'
,
'mega-button'
);
// megaButton instanceof MegaButton === true
Comme vous pouvez le voir, il existe maintenant une version enrichie de document.createElement() qui prend en second paramètre la valeur de l'attribut is="".
Utilisez l'opérateur new :
Jusqu'à présent, nous n'avons utilisé que document.register() pour ajouter de nouveaux éléments, mais cela ne va pas très loin… alors ajoutons des propriétés et des méthodes.
IV. Ajouter des propriétés et des méthodes JavaScript▲
La puissance des éléments créés par l'utilisateur réside dans le fait de pouvoir leur attribuer des fonctionnalités particulières en leur attribuant des propriétés et des méthodes spécifiques dans leur définition. Cela revient à fournir une API publique liée à vos éléments.
Voici un exemple complet :
var XFooProto =
Object.create
(
HTMLElement.
prototype);
// 1. Donner à x-foo une méthode foo().
XFooProto.
foo =
function(
) {
alert
(
'foo() called'
);
};
// 2. Définir une propriété en lecture seule "bar".
Object.defineProperty
(
XFooProto,
"bar"
,
{
value
:
5
}
);
// 3. Enregistrer la définition de x-foo.
var XFoo =
document
.register
(
'x-foo'
,
{
prototype
:
XFooProto}
);
// 4. Instancier un objet x-foo.
var xfoo =
document
.createElement
(
'x-foo'
);
// 5. L'ajouter à la page.
document
.
body.appendChild
(
xfoo);
Bien sûr, il existe une multitude de façons de créer un prototype. Si la version précédente n'est pas votre préférée, en voici une plus condensée :
var XFoo =
document
.register
(
'x-foo'
,
{
prototype
:
Object.create
(
HTMLElement.
prototype,
{
bar
:
{
get
:
function(
) {
return 5
;
}
},
foo
:
{
value
:
function(
) {
alert
(
'foo() called'
);
}
}
}
)
}
);
Le premier exemple utilise la méthode ES5EcmaScript 5 Object.defineProperty, la seconde utilise les méthodes get/set.
IV-A. Les fonctions de rappel au cours du cycle de vie▲
Les éléments permettent de définir des méthodes spéciales liées aux moments importants de leur mise en œuvre. Ces méthodes sont appelées de façon avisée « fonctions de rappel au cours du cycle de vie » (lifecycle callbacks). Chacune a un nom et une utilité spécifique.
Nom de la fonction de rappel |
Appelée quand… |
createdCallback |
une instance de l'élément est créée |
enteredDocumentCallback |
une instance est intégrée dans le document |
leftDocumentCallback |
une instance est retirée du document |
attributeChangedCallback(attrName, oldVal, newVal) |
un attribut a été ajouté, supprimé ou modifié |
Dans l'exemple suivant, nous valorisons les fonctions de rappel createdCallback() et enteredDocumentCallback() sur la balise <x-foo> :
var proto =
Object.create
(
HTMLElement.
prototype);
proto.
createdCallback =
function(
) {...};
proto.
enteredDocumentCallback =
function(
) {...};
var XFoo =
document
.register
(
'x-foo'
,
{
prototype
:
proto}
);
Chacune de ces fonctions de rappel est optionnelle, ne les définissez que si cela est utile. Par exemple, imaginons un élément particulièrement complexe qui ouvre une connexion à IndexedDB dans createdCallback(). Dans ce cas, n'oubliez pas, s'il doit être retiré du DOM, de faire le ménage nécessaire dans leftDocumentCallback().
Note
Vous ne devriez pas utiliser ces méthodes si l'utilisateur ferme un onglet par exemple, voyez-les plutôt comme des possibilités d'optimisation.
Un autre cas classique d'utilisation est de définir des gestionnaires d'événements par défaut :
proto.
createdCallback =
function(
) {
this.addEventListener
(
'click'
,
function(
e) {
alert
(
'Thanks!'
);
}
);
};
Personne n'utilisera vos éléments s'ils sont bancals, les fonctions de rappel du cycle de vie vous aideront à être un bon citoyen !
V. Ajouter du balisage▲
Nous avons maintenant créé un élément <x-foo> et lui avons fourni une API, mais il est encore vide ! Peut-être pourrions-nous lui donner un peu de HTML à afficher ?
Les fonctions de rappel du cycle de vieLes fonctions de rappel au cours du cycle de vie sont très utiles pour ça. En particulier, nous pouvons utiliser createdCallback() pour doter un élément de code HTML par défaut à afficher :
var XFooProto =
Object.create
(
HTMLElement.
prototype);
XFooProto.
createdCallback =
function(
) {
this.
innerHTML =
"<b>Je suis un x-foo-with-markup !</b>"
;
};
var XFoo =
document
.register
(
'x-foo-with-markup'
,
{
prototype
:
XFooProto}
);
Si nous instancions cette balise et que nous l'inspectons dans la console (clic droit puis « Inspecter l'élément »), nous devrions voir :
▼
<x-foo-with-markup>
<b>Je suis un x-foo-with-markup !</b>
</x-foo-with-markup>
V-A. Encapsuler le contenu dans le DOM fantôme▲
En soi, le DOM fantôme (Shadow DOM) est un outil puissant pour encapsuler du contenu. Utilisé en conjonction avec les éléments personnalisés et la magie prend forme !
Le DOM fantôme permet aux éléments personnalisés :
- de cacher leurs entrailles, permettant ainsi de masquer le cambouis de l'implémentation aux utilisateurs ;
- d'encapsuler les styles… et gratuitement.
Créer un élément dans le DOM fantôme lui permet de s'afficher selon un balisage de base. La différence se faisant dans createdCallback() :
var XFooProto =
Object.create
(
HTMLElement.
prototype);
XFooProto.
createdCallback =
function(
) {
// 1. Attacher une racine fantôme à l'élément.
var shadow =
this.createShadowRoot
(
);
// 2. Le remplir avec le bon balisage.
shadow.
innerHTML =
"<b>Je suis dans le DOM fantôme de l'élément !</b>"
;
};
var XFoo =
document
.register
(
'x-foo-shadowdom'
,
{
prototype
:
XFooProto}
);
Au lieu d'affecter le .innerHTML de l'élément, j'ai créé une racine fantôme (Shadow Root) pour <x-foo-shadowdom> et y ai ajouté le balisage. En activant l'option « Show Shadow DOM » dans les outils de développeur de Chrome (NdT : seul navigateur supportant cette API au moment de l'écriture de l'article), vous verrez un #document-fragment que vous pouvez déplier :
▼
<x-foo-shadowdom>
▼
#document-fragment
<b>Je suis dans le DOM fantôme de l'élément !</b>
</x-foo-shadowdom>
C'est cela la racine fantôme !
V-B. Créer des éléments à partir d'un gabarit▲
Les gabarits HTML (HTML Templates) sont une autre nouvelle API qui convient parfaitement au monde des éléments personnalisés.
Pour ceux qui ne seraient pas familiers avec la balise <template>, celle-ci permet de déclarer des fragments de DOM qui sont parsés, inactifs au chargement de la page et destinés à être instanciés ultérieurement. Elles sont donc l'emplacement idéal pour déclarer la structure d'un élément personnalisé.
Dans cet exemple, nous enregistrons un élément créé à partir d'un <template> dans le DOM fantôme :
<template id=
"sdtemplate"
>
<style
>
p {
color:
orange;
}
</style>
<p>
Je suis dans le DOM fantôme. Mon balisage provient d'un <template>
.</p>
</template>
<script
>
var proto =
Object.create
(
HTMLElement.
prototype,
{
createdCallback
:
{
value
:
function(
) {
var t =
document
.querySelector
(
'#sdtemplate'
);
this.createShadowRoot
(
).appendChild
(
t.
content.cloneNode
(
true));
}
}
}
);
document
.register
(
'x-foo-from-template'
,
{
prototype
:
proto}
);
</script>
Cette portion de code contient beaucoup de choses. Essayons de détailler tout ce qu'il s'y passe.
- Nous enregistrons un nouvel élément HTML, <x-foo-from-template>.
- Le contenu DOM de cet élément est créé à partir d'un <template>.
- La partie interne est masquée à l'aide du DOM fantôme.
- Le DOM fantôme encapsule le style de l'élément (la règle CSS p {color: orange;} ne rend pas tous les paragraphes orange).
C'est beau !
VI. Donner du style aux éléments personnalisés▲
Comme pour toute balise HTML, les utilisateurs vont pouvoir appliquer des styles CSS à vos éléments :
app-panel {
display:
flex
;
}
[
is=
"x-item"
]
{
transition:
opacity 400
ms 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:
5
px;
list-style:
none
;
margin:
0
7
px;
}
<app-panel>
<li is
=
"x-item"
>
Do</li>
<li is
=
"x-item"
>
Re</li>
<li is
=
"x-item"
>
Mi</li>
</app-panel>
VI-A. Style des éléments du DOM fantôme▲
Le terrier du lapin devient beaucoup plus profond lorsque le DOM fantôme entre en jeu et les éléments personnalisés qui l'utilisentEncapsuler le contenu dans le DOM fantôme profitent de ses larges avantages.
Le DOM fantôme est parfaitement cloisonné, y compris ses déclarations de style. Les styles CSS définis à partir de la racine fantôme ne peuvent pas s'appliquer sur le reste de la page, de même, les styles de la page n'ont pas d'effet sur le DOM fantôme. Dans le cas des éléments personnalisés, l'élément lui-même est la racine. L'encapsulation des styles permet aussi à un élément personnalisé de définir ses propres styles par défaut.
La mise en forme du DOM fantôme est un immense sujet ! Si vous souhaitez en savoir plus à ce propos, je vous recommande certains de mes articles :
- A Guide to Styling Elements dans la documentation de Polymer ;
- Shadow DOM 201 : CSS & Styling sur HTML5 Rocks.
VI-B. Éviter le FOUC avec :unresolved▲
Pour limiter le FOUCFlash of unstyled content, la spécification des éléments personnalisés introduit le nouveau pseudoélément :unresolved. Vous pouvez l'utiliser pour cibler les éléments non résolusÉléments non résolus jusqu'à ce que le navigateur n'invoque votre fonction createdCallback() (voir Les fonctions de rappel au cours du cycle de vieLes fonctions de rappel au cours du cycle de vie). Une fois cette fonction appelée, l'élément n'est plus « non résolu », le processus de mise à jour est achevé et l'élément est transformé conformément à sa déclaration.
Le pseudoélément :unresolved est supporté nativement dans Chrome 29.
Exemple : faire apparaître progressivement les balises <x-foo>
x-foo {
opacity:
1
;
transition:
opacity 300
ms;
}
x-foo:
unresolved {
opacity:
0
;
}
Gardez bien à l'esprit que le pseudoélément :unresolved s'applique uniquement aux éléments non résolusÉléments non résolus, pas aux éléments héritant de l'interface HTMLUnknownElement (voir Comment sont gérés les élémentsComment sont gérés les éléments).
<style>
/* appliquer une bordure pointillée aux éléments non résolus */
:
unresolved {
border:
1
px dashed
red
;
display:
inline-block
;
}
/* les éléments x-panel non résolus sont en rouge */
x-panel:
unresolved {
color:
red
;
}
/* les éléments x-panel enregistrés sont en vert */
x-panel {
color:
green
;
display:
block
;
padding:
5
px;
}
</style>
<panel>
Je suis en noir, car le pseudoélément :unresolved ne s'applique pas à "panel".
Ce n'est pas un nom valide pour un élément personnalisé.
</panel>
<x-panel>Je suis en rouge, car x-panel:unresolved s'applique à moi.</x-panel>
Pour en savoir plus sur :unresolved, voir A Guide to styling elements sur Polymer.
VII. Historique et support des navigateurs▲
VII-A. Détection de fonctionnalité▲
Détecter la disponibilité de l'API revient à vérifier l'existence de document.register() :
VII-B. Support des navigateurs▲
document.register() a été implémenté dans Chrome 27 et Firefox 23 en activant une option. Cependant, la spécification a quelque peu évolué entre temps et Chrome 31 est le premier à assurer un réel support de la version actuelle de la spécification.
Pour activer la prise en compte des éléments personnalisés dans Chrome 31, tapez chrome://flags/ dans la barre d'adresse et activez l'option « Activer les fonctionnalités expérimentales de Web Platform » (chrome://flags/#enable-experimental-web-platform-features).
En attendant un support plus significatif, vous pouvez utiliser ces solutions de remplacement :
- Polymer, de Google propose une solution ;
- les x-tags de Mozilla.
VII-C. Qu'est devenu HTMLElementElement ?▲
Ceux qui ont suivi le travail de standardisation savent qu'il a existé la balise <element>. C'était vraiment la panacée. Vous pouviez les utiliser pour enregistrer les nouveaux éléments de façon déclarative :
<element name
=
"my-element"
>
...
</element>
Malheureusement, il y avait trop de problèmes de performances au niveau du processus de mise à jourComment sont gérés les éléments, des effets de bord et des scénarios catastrophe qui ont empêché cela de fonctionner correctement. Ainsi, <element> a dû être abandonné. En août 2013, Dimitri Glazkov annonça sa suppression dans un message du public-webapps, en tout cas pour le moment…
Il est toutefois intéressant de noter que Polymer implémente une approche déclarative pour l'enregistrement d'éléments avec <polymer-element>. Comment cela est-il mis en place ? En utilisant document.register('polymer-element') et les techniques présentées dans Créer des éléments à partir d'un gabaritCréer des éléments à partir d'un gabarit
VIII. Conclusion et remerciements▲
Les éléments personnalisés nous fournissent un outil permettant d'enrichir le vocabulaire du HTML, de lui apprendre de nouvelles choses et d'enrichir les applications Web. En les combinant avec d'autres interfaces comme le DOM fantôme ou <template>, nous commençons à avoir un bel aperçu des composants Web (Web Components). De cette façon, le balisage peut de nouveau être élégant !
Si vous êtes intéressé par les composants Web, je vous invite à regarder du côté de Polymer, qui apporte plus que le strict minimum pour démarrer.
Cet article est la traduction de Custom Elements : defining new elements in HTML publié sur le site HTML5 Rocks.
Nous tenons à remercier f-leb et Phanloga pour leur relecture attentive de cet article.
N'hésitez pas à commenter cet article sur le forum. 3 commentaires