Il y a pas mal de façons de faire et c’est une combinaison de ce qu’on pu partager récemment les influenceurs tech de ma bulle (surtout Zach Leat et Chris Ferdinandi). Merci à Anthony pour la partie sur l’écoute des attributs !
// (1)!consttemplate=document.createElement('template')template.innerHTML=`<button>Hello world</button>`classMyComponentextendsHTMLElement{staticregister(tagName='my-component'){// (2)!if('customElements'inwindow&&!customElements.get(tagName)){customElements.define(tagName,this)}}// (3)!staticobservedAttributes=['color','size']constructor(){super()this.attachShadow({mode:'open'})// (4)!}handleEvent(event){// (5)!this[`on${event.type}`](event)}onclick(event){// Do something when the component is clicked.}oninput(event){// Same with the input event.}connectedCallback(){this.shadowRoot.appendChild(template.content.cloneNode(true))this.addEventListener('click',this)this.addEventListener('input',this)}disconnectedCallback(){// (6)!this.removeEventListener('click',this)this.removeEventListener('input',this)}// (7)!attributeChangedCallback(name,oldValue,newValue){console.log(`Attribute ${name} has changed from ${oldValue} to ${newValue}.`)}}MyComponent.register()
Il s’agit d’un exemple avec un template dynamique, si le HTML peut être ajouté directement dans le Web Component c’est mieux car plus résilient si le JS plante au chargement par exemple…
J’aime bien le fait que le composant puisse s’auto-enregistrer tout en laissant la faculté de personnaliser le nom. C’est optionnel.
On définit les attributs dont on veut observer les changements.
Ça va automagiquement ajouter un this.shadowRoot, voir cet article détaillé de Lee James Reamsnyder. On le met ouvert pour pouvoir jouer avec en JavaScript par la suite.
Astuce pour dispatcher les évènements vers des méthodes dédiées. Voir l’article de Andrea Giammarchi pour comprendre la méthode standard handleEvent sur un objet.
C’est une bonne pratique de déconnecter les écouteurs rendus inutiles, a fortiori sur des web components !
Si l’attribut color ou size change sur le composant, on passe là-dedans, magique !
Par ici un exemple assez brut car c’est toujours chouette de jouer avec en direct :
Les Web Components étant chargés en JavaScript, il y a un moment où il
peut y avoir un flash lors du chargement.
Cette astuce proposée par Cory LaViska permet de donner une
impression de chargement synchrone à la page.
<style>body{opacity:0;}body.ready{opacity:1;transition:0.25sopacity;}</style><scripttype="module">awaitPromise.allSettled([customElements.whenDefined('my-button'),customElements.whenDefined('my-card'),customElements.whenDefined('my-rating'),])// Button, card, and rating are registered now! Add// the `ready` class so the UI fades in.document.body.classList.add('ready')</script>
On attend que chacun des composants soit chargé pour afficher le
<body> avec une transition sur l’opacité. Selon la portée de
vos composants, il n’est pas forcément nécessaire d’appliquer cela à
la page entière mais ça donne des idées.
Si vous reprenez deux des astuces précédentes pour l’enregistrement et les évènements, il peut être intéressant de ne pas avoir à les répéter pour chaque définition de composant !
Ici on crée des mixin qui vont permettre d’étendre le comportement par défaut de HTMLElement.
Merci à Luke Secomb.
exportclassDefinedElementextendsHTMLElement{constructor(){super()// Do something.console.log('Constructed.')}}if(newURL(import.meta.url).searchParams.has('define')){customElements.define('defined-element',DefinedElement)}
Ce qui permet ensuite de charger le composant avec une auto-définition :
Simple et efficace (vérifier le support de import() pour votre convenance bien sûr).
Un innerHTML moderne avec évaluation du shadow DOM⚓︎
2024-10
Une nouvelle méthode découverte en explorant le code derrière cet article qui permet de faire en sorte que le declarative shadow DOM soit interprété depuis une chaîne de texte renvoyée par le serveur :
constfetchPartial=async(path)=>{constresponse=awaitfetch(`/${path}`)consthtml=awaitresponse.text()consttmpl=document.createElement('template')// This is a new API, see: https://thathtml.blog/2024/01/dsd-safety-with-set-html-unsafe/// and: https://caniuse.com/mdn-api_element_sethtmlunsafeif('setHTMLUnsafe'intmpl){tmpl.setHTMLUnsafe(html)}else{// Fall back on using innerHTML for older browsers.tmpl.innerHTML=html}returntmpl.content}
C’est du baseline 2024 mais si vous travaillez avec des Web Components + shadow DOM c’est plutôt essentiel.