Standardiser les modèles côté client
Introduction
Le concept de création de modèles n'est pas nouveau dans le développement Web. En fait, les moteurs/langages de création de modèles côté serveur tels que Django (Python), ERB/Haml (Ruby) et Smarty (PHP) existent depuis longtemps. Toutefois, ces dernières années, nous avons assisté à une explosion des frameworks MVC. Ils sont tous légèrement différents, mais la plupart partagent un mécanisme commun pour afficher leur couche de présentation (la vue) : les modèles.
Soyons réalistes. Les modèles sont fantastiques. Allez-y, demandez autour de vous. Même sa définition donne une impression de chaleur et de confort :
"… n'a pas besoin d'être recréé à chaque fois…" Je ne sais pas pour vous, mais j'adore éviter le travail supplémentaire. Pourquoi la plate-forme Web ne propose-t-elle pas de compatibilité native pour une fonctionnalité qui intéresse clairement les développeurs ?
La spécification des modèles HTML WhatWG est la réponse. Il définit un nouvel élément <template> qui décrit une approche standard basée sur le DOM pour la création de modèles côté client. Les modèles vous permettent de déclarer des fragments de balisage qui sont analysés en tant que code HTML, ne sont pas utilisés lors du chargement de page, mais peuvent être instanciés ultérieurement au moment de l'exécution. Pour citer Rafael Weinstein :
Ils permettent de placer un gros bloc de code HTML que vous ne voulez pas que le navigateur modifie, pour quelque raison que ce soit.
Rafael Weinstein (auteur de la spécification)
Détection de caractéristiques
Pour détecter la fonctionnalité <template>, créez l'élément DOM et vérifiez que la propriété .content existe :
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
Déclarer le contenu du modèle
L'élément HTML <template> représente un modèle dans votre balisage. Il contient le "contenu du modèle", c'est-à-dire des morceaux inertes de DOM clonable.
Considérez les modèles comme des éléments d'échafaudage que vous pouvez utiliser (et réutiliser) tout au long du cycle de vie de votre application.
Pour créer un contenu basé sur un modèle, déclarez un balisage et encapsulez-le dans l'élément <template> :
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
Les piliers
Encapsuler du contenu dans un <template> nous donne quelques propriétés importantes.
Son contenu est inerte jusqu'à son activation. En gros, votre balisage est un DOM masqué et ne s'affiche pas.
Le contenu d'un modèle n'aura pas d'effets secondaires. Le script ne s'exécute pas, les images ne se chargent pas et l'audio n'est pas lu… tant que le modèle n'est pas utilisé.
Le contenu est considéré comme ne figurant pas dans le document. L'utilisation de
document.getElementById()ou dequerySelector()sur la page principale ne renvoie pas les nœuds enfants d'un modèle.Les modèles peuvent être placés n'importe où dans
<head>,<body>ou<frameset>, et peuvent contenir n'importe quel type de contenu autorisé dans ces éléments. Notez que "n'importe où" signifie que<template>peut être utilisé en toute sécurité dans des endroits que l'analyseur HTML n'autorise pas… tous sauf les enfants du modèle de contenu. Il peut également être placé en tant qu'enfant de<table>ou de<select>:
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
Activer un modèle
Pour utiliser un modèle, vous devez l'activer. Sinon, son contenu ne s'affichera jamais.
Le moyen le plus simple consiste à créer une copie complète de son .content à l'aide de document.importNode(). La propriété .content est un DocumentFragment en lecture seule contenant les éléments du modèle.
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
Une fois qu'un modèle est appliqué, son contenu est mis en ligne. Dans cet exemple, le contenu est cloné, la requête d'image est effectuée et le balisage final est affiché.
Démonstrations
Exemple : script inerte
Cet exemple illustre l'inertie du contenu du modèle. Le <script> ne s'exécute que lorsque l'utilisateur appuie sur le bouton, ce qui permet de créer le modèle.
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
Exemple : Créer un Shadow DOM à partir d'un modèle
La plupart des utilisateurs associent le Shadow DOM à un hôte en définissant une chaîne de balisage sur .innerHTML :
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
Le problème avec cette approche est que plus votre Shadow DOM devient complexe, plus vous effectuez de concaténations de chaînes. Ce n'est pas évolutif, les choses se compliquent rapidement et les bébés se mettent à pleurer. C'est d'ailleurs ainsi que les attaques XSS sont nées. <template> à la rescousse.
Une approche plus saine consisterait à travailler directement avec le DOM en ajoutant le contenu du modèle à une racine fantôme :
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas
Voici quelques pièges que j'ai rencontrés en utilisant <template> dans la pratique :
- Si vous utilisez modpagespeed, faites attention à ce bug. Les modèles qui définissent des éléments
<style scoped>intégrés peuvent être déplacés vers l'en-tête à l'aide des règles de réécriture CSS de PageSpeed. - Il n'est pas possible de "pré-afficher" un modèle, ce qui signifie que vous ne pouvez pas précharger des éléments, traiter du code JS, télécharger du code CSS initial, etc. Cela vaut aussi bien pour le serveur que pour le client. Un modèle n'est affiché que lorsqu'il est mis en ligne.
Faites attention aux modèles imbriqués. Ils ne se comportent pas comme vous pourriez vous y attendre. Exemple :
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>L'activation du modèle externe n'activera pas les modèles internes. En d'autres termes, les modèles imbriqués nécessitent que leurs enfants soient également activés manuellement.
La route vers une norme
N'oublions pas d'où nous venons. Le chemin vers des modèles HTML basés sur des normes a été long. Au fil des ans, nous avons trouvé des astuces assez ingénieuses pour créer des modèles réutilisables. En voici deux que j'ai rencontrés fréquemment. Je les inclus dans cet article à des fins de comparaison.
Méthode 1 : DOM hors écran
Une approche utilisée depuis longtemps consiste à créer un DOM "hors écran" et à le masquer à l'aide de l'attribut hidden ou de display:none.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
Bien que cette technique fonctionne, elle présente un certain nombre d'inconvénients. Voici un résumé de cette technique :
- Utiliser le DOM : le navigateur connaît le DOM. Il est très efficace. Nous pouvons facilement le cloner.
- Rien n'est affiché : l'ajout de
hiddenempêche l'affichage du bloc. - Non inerte : même si notre contenu est masqué, une requête réseau est toujours envoyée pour l'image.
- Style et thèmes complexes : une page d'intégration doit ajouter le préfixe
#mytemplateà toutes ses règles CSS afin de limiter les styles au modèle. Cette approche est fragile, et rien ne garantit que nous ne rencontrerons pas de futurs conflits de noms. Par exemple, nous sommes bloqués si la page d'intégration comporte déjà un élément avec cet ID.
Méthode 2 : Script de surcharge
Une autre technique consiste à surcharger <script> et à manipuler son contenu sous forme de chaîne. John Resig a probablement été le premier à le démontrer en 2008 avec son utilitaire de micro-modèles.
Il en existe désormais de nombreux autres, y compris de nouveaux venus comme handlebars.js.
Exemple :
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
Voici un résumé de cette technique :
- Rien n'est affiché : le navigateur n'affiche pas ce bloc, car
<script>est défini surdisplay:nonepar défaut. - Inert : le navigateur n'analyse pas le contenu du script en tant que JS, car son type est défini sur une valeur autre que "text/javascript".
- Problèmes de sécurité : encourage l'utilisation de
.innerHTML. L'analyse des chaînes d'exécution des données fournies par l'utilisateur peut facilement entraîner des failles XSS.
Conclusion
Vous vous souvenez de l'époque où jQuery simplifiait considérablement l'utilisation du DOM ? Le résultat a été l'ajout de querySelector()/querySelectorAll() à la plate-forme. C'est une victoire évidente, n'est-ce pas ? Une bibliothèque a popularisé la récupération du DOM avec des sélecteurs CSS, et les normes l'ont ensuite adoptée. Ça ne se passe pas toujours comme ça, mais j'adore quand c'est le cas.
Je pense que <template> est un cas similaire. Il standardise la façon dont nous créons des modèles côté client, mais surtout, il élimine le besoin de nos hacks de 2008.
Rendre l'ensemble du processus de création Web plus sain, plus facile à gérer et plus complet est toujours une bonne chose à mon sens.
Ressources supplémentaires
- Spécification WhatWG
- Présentation des composants Web
- <web>components</web> (vidéo) : une présentation incroyablement complète par votre serviteur.