C’est en travaillant sur un projet d’éditeur de contenu en ligne que je suis tombé sur le texte de Chris Coyier, Playing with Shadow DOM (CSS-Tricks). Le texte soulignait l’initiative de Twitter d’utiliser le Shadow DOM pour pousser son contenu.
Le « Trick » étant de servir un Shadow DOM aux navigateurs compatibles (Chrome et Opera) et un bon vieux iFrame aux autres retardataires, et j’ai particulièrement nommé Internet Explorer et même Edge.
« Twitter appuie sa décision sur une meilleure utilisation de la mémoire par le navigateur et un temps de rendu beaucoup plus rapides. C’est à dire des tweets plus rapidement et un défilement plus doux. »
La version 1.0 fonctionne sous safari à part quelques sélecteurs :host > .local-child
et ::slotted
. Firefox v63+ supporte le Shadow DOM 1.0 mais il faut l’activer, question de sécurité !!
- Entrer dans l’adresse : about:config
- Rechercher la propriété : dom.webcomponents.enabled
- Activer la propriété : dom.webcomponents.enabled ou dans mon cas dom.webcomponents.shadowdom.enabled
Version 1.0 car il y avait la version zéro ; -))
Mais oubliez la tout de suite, moins performant et surtout très peu supporté. Comparer la création d’un Shadow DOM :
// v0 let e = document.createElement('div'); let shadowRoot = e.createShadowRoot(); // v1 let e = document.createElement('div'); let shadowRoot = e.attachShadow({ mode: 'open' });
Styles délimités dit « scoped »
Éléments personnalisés inutiles
Les « éléments personnalisés » sont vraiment cutes, mais aussi complètement inutiles. En fait pour s’accorder avec la doctrine sémantique (;-), un élément personnalisé valable devrait de facto être transformé en balise officielle par la W3C !
Par contre, le modèle ou le gabarit (Templates), déjà plus compatible, peut devenir très pratique pour un développeur. Du simple stockage de donnée à l’encapsulation d’application des plus sophistiqués. Mon prochain tutoriel...
Personnellement (et pour une fois), l’optimisation de la performance au niveau du navigateur n’est pas ma principale motivation. Mon problème et celui de la plupart des développeurs, c’est le contenu tiers. Particulièrement son intégration au DOM sans toute fois partager ces propriétés et ses classes CSS. Le JavaScript permet d’intégrer un contenu distinct. Mais pour l’isoler complètement du DOM, il n’y a que la solution du iFrame. Avec tous les inconvénients que cela implique. Bon, inconvénients pour la plupart réglés depuis le temps, comme la hauteur ou les proportions d’un iFrame, mais ils n’empêchent que cette vielle méthode jure en boyenne dans la logique du Document Object Model (DOM) !
Un peu à la manière de feu l’attribue <style scoped>
, supprimé des spécifications de la W3C (même les navigateurs qui le supportaient l’ont retiré). C’est-à-dire la possibilité d’appliquer des styles délimités à un bloc du DOM. Par exemple dans mon cas : une feuille de style particulière à mon éditeur HTML indépendante du style du SGC qui l’utilise.
En plus du Shadow DOM et du iFrame toujours obligatoire, cette technique ce présente souvent avec d’autres éléments : Les « éléments personnalisés » (Custom Elements) et les « Modèles » (Templates). Mais sachez qu’ils ne sont pas obligatoires dans le cadre de ce tutoriel.
À quoi ça sert ?
Je ne suis pas d’accord avec la définition de MDN à savoir « ...si le CSS n’est pas correctement organisé, les styles de certains composants peuvent impacter d’autres parties du site bien que ce ne soit pas prévu, et vice-versa. ». ...si le CSS n’est pas correctement organisé !! Une technique pour les mauvaises programmeur ;- ) Par contre le contenu d’un widget Facebook ou Twitter est totalement imprévisible. Imaginez les gars essayer de créer un code compatible avec le style de chaque site Web. De là l’usage du iFrame, toujours indépendant aux styles globaux du site.
Le Shadow DOM fait exactement la même chose, mais en intégrant parfaitement, et de façon totalement indépendante, sa branche dans l’arbre du DOM. C’est-à-dire qu’on peut ensuite le manipuler à volonté et de façon dynamique, ajouter des nœuds. des évènements, modifier le contenu, le style, etc. Par exemple récupérer le contenu d’un Shadow DOM :
// On accède toujours au Shadow par sa racine : shadowRoot document.getElementById("monobjetshadow").shadowRoot.innerHTML;
Créer un objet « Shadow »
Un Shadow DOM doit toujours être lié à un élément existant ou produit par un script dit l’« hôte ». L’hôte doit être membre du DOM. Si l’on pouvait utiliser la plupart des balises avec la version 0, par exemple « input » !! La version 1 limite ce choix, avec raison :
- article
- aside
- blockquote
- body
- div
- footer
- h1
- h2
- h3
- h4
- h5
- h6
- header
- main
- nav
- p
- section
- span
- Plus les éléments personnalisés (Custom Elements).
Pour attacher le Shadow DOM à l’élément hôte il suffit de faire Element.attachShadow()
comme dans cet exemple :
<div id="oContenu_isole">Votre navigateur ne supporte pas le « Shadow DOM »</div> <script> // Créer le Shadow DOM sur l'élément <div> var shadow = document.querySelector('#oContenu_isole') .attachShadow({mode: "open"}) .innerHTML = '<span">Contenu isolé du DOM principal !</span>' ; </script>
Avertissement
Pour une compatibilité avec Chrome 34 et moins, vous devez préfixer le createShadowRoot
avec « webkit ». Tous les appels à createShadowRoot
devraient être host.webkitCreateShadowRoot()
. P>
Speak White
Mais attention, n’est pas compatible qui le veut. Au moment d’écrire ce texte, mon exemple ne fonctionnait pas ! Avant de réaliser que j’avais un caractère, ou plutôt deux caractères tellement illégaux qu’ils provoquaient une erreur. Je n’ai pas testé les milliers de caractères UTF-8, mais chose sûr, les caractères « et » font bizarrement planter ce JavaScript ! Ha les guillemets !
L’exemple ci-dessus ajoute un « Shadow DOM » à l’élément hôte <div>
puis ajoute un nouveau contenu. Pourquoi « open » ? Pour rendre accessible le contenu du Shadow à partir de sa racine (shadowRoot) en mode « close » shadowRoot
retourne « null ». C’est pourquoi on utilise pratiquement toujours « open ».
L’exemple suivant est le même, mais il utilise sa propre feuille de style exclusivement dédié au <div>. C’est-à-dire la raison première de son utilité :
<div id="oContenu_isole">Votre navigateur ne supporte pas le « Shadow DOM »</div> <script> // Créer le Shadow DOM sur l'élément <div> var oShadow = document.querySelector('#oContenu_isole') .attachShadow({mode: "open"}) .innerHTML = '<style>div {color:red;}</style><span>Contenu isolé du DOM principal en rouge !</span>' ; </script>
Styles et sélecteurs CSS
Pseudo-classes CSS
- defined
- host
- host()
- host-context()
Styliser l’élément Shadow Root à partir de la page parent
L’élément hôte peut facilement être stylisé n’importe où dans la page parent, car l’élément hôte ne fait pas partie du Shadow DOM. Mais comment modifier le style de cet hôte en fonction du contenu du Shadow DOM ? Il existe un nouveau sélecteur « host » qui permet d’accéder à l’hôte du Shadow DOM depuis sa racine :
<style>
:host {
background-color:#ffffff;
}
</style>
Le pseudo-éléments ::shadow
permet de cibler la racine du Shadow DOM et ses éléments enfants. Par exemple ici les paragraphes :
<style>
#host::shadow p {color: #555555;}
</style>
Pour appliquer un thème à une application Shadow DOM entière, utiliser le drôle de combinateur >>>
(anciennement /deep/). Le combinateur >>>
permet de croiser toutes les racines d’ombre avec un seul sélecteur : (NOTE - Inutile avec la version 1...)
<style>
body >>> p {
color: blue;
}
</style>
Styliser l’élément Shadow Root à partir du Shadow Root
Les styles hérités héritent toujours des styles du DOM global. C’est pourquoi l’isolement n’est pas aussi efficace que le iFrame. Il faut donc obligés le Shadow DOM à revenir à l’état initial. En reprenant mon exemple :
<div id="oContenu_isole">Votre navigateur ne supporte pas le « Shadow DOM »</div>
<script>
// Créer le Shadow DOM sur l’élément <div>
var oShadow = document.querySelector('#oContenu_isole')
.attachShadow({mode: "open"})
.innerHTML = '<style>:host {all: initial;}</style><span>Contenu isolé du DOM principal en rouge !</span>'
;
</script>
Éditeur visuel
Maintenant on peut y mettre tout ce que l’on désire. Du plus complexe JavaScript à la plus banale petite classe CSS. Dans mon exemple, j’ai besoin de donner un aperçu du code saisi dans un textarea. Bien entendu je ne veux pas qu’il soit impacté par le CSS global de la page, mais en plus je désire lui appliquer le style perso du site sur lequel cette page sera affichée. Bien entendu, il n’est malheureusement pas question de charger une feuille de style externe à partir d’un autre domaine. Ça reste une simulation, mais tout à fait acceptable.
Imaginez, mon CMS Neural utilise un texte blanc sur un fond foncé. En conséquence et en tenant compte de l’héritage des styles globaux, si j’affiche le texte saisi dans un <div>
neutre avec un fond blanc, le texte héritera de la couleur blanche globale et sera donc invisible sur le fond blanc ! Dans un iFrame (ou un popup) le style global de la page parente ne s’applique pas alors le problème ne se pose pas. J’utilise donc un Shadow DOM pour forcer la bonne couleur.
Bien sûr on peut utiliser des sélecteurs bien précis pour l’ensemble de la page limité à un objet (#MonObjet) et ainsi les isoler d’un possible contenu externe. Par exemple un objet #MonObjet p {...}
, #MonObjet .maClasse {...}
. Mais encore faut-il que ce tiers contenu soit déclaré à l’extérieur de l’objet en question. Ce qui limite les choix. Et surtout c’est pratiquement impossible. Les balises natives du HTML global gardent toujours leur propriété respective.
let sStyleTiers = `<style>body, p, td, li, div {color:red;}</style>`; oShadow.innerHTML = sStyleTiers+' Contenu isolé du DOM principal en rouge';
L’indispensable iFrame
Si vous utilisez Internet Explorer ou un vieux navigateur, Twitter vous poussera quand même un iFrame au lieu du Shadow DOM. Et c’est là, à l’instar de Twitter, que la possibilité d’utiliser cette nouvelle technologie devient possible et très intéressante. Obtenir les capacités du Shadow Dom, sinon, au pire, utiliser le bon vieux iFrame. Alors, pourquoi s’en priver?
<div id="oContenu_isole"><p>Votre navigateur ne supporte pas le « Shadow DOM » !</p></div> <style> iframe {border: 0;width: 100%;padding: 0;} </style> <script> let sStyleTiers = ` <style> :host {all:initial;} body, p, td, li, div {color:red;} </style> <p>Votre navigateur supporte le Shadow DOM !</p> `; let el = document.querySelector('#oContenu_isole'); if (document.body.attachShadow) { // Si compatible Shadow DOM let oShadow = el.attachShadow({ mode: 'open' }); oShadow.innerHTML = sStyleTiers; } else { // Sinon utiliser un iFrame let oFrame = document.createElement('iframe'); oFrame.id = 'oFrame'; 'srcdoc' in oFrame ? oFrame.srcdoc = sStyleTiers : oFrame.src = 'data:text/html;charset=UTF-8,' + sStyleTiers; // Et on remplace le DIV par le iframe let parent = el.parentNode; parent.replaceChild(oFrame, el); } </script>
L’exemple permet, dans le cas d’incompatibilité avec le Shadow DOM, de créer à la volé un iFrame et de le remplir avec le contenu. Puis de simplement remplacer le DIV du Shadow par le iFrame en question. Avec le petit fixe de Šime Vidas. Vous pouvez aussi attacher une feuille de styles au iFrame :
Conclusion
Ce tutoriel ne trace que les grandes lignes de cette technique. Plusieurs aspects n’ont pas été traités, par exemple : DocumentOrShadowRoot
, template
, content
, slot
et les sélecteurs ::slotted
, :host-context()
, :host()
.
Références
- Shadow DOM - W3C
- Chapter 5. Working with the Shadow DOM - Modern JavaScript par O’Reilly Media, Inc.
- What’s New in Shadow DOM v1 (by examples) - hayato.io
- Everything you need to know about Shadow DOM par Praveen Puglia
- Shadow DOM - Web Components - MDN
- Articles tagged: shadow dom - MDN Web Docs
- A Guide to Web Components par Rob Dodson - CSS-Tricks
- Playing with Shadow DOM par Chris Coyier - CSS-Tricks
- HTML scoped Attribute - W3Schools
- HTML <template> Tag - W3Schools
- Album « Outside the Shadow of an Aliquot Tree » par Chvad SB