Nouvelle balise de modèle HTML

Normalisation de la création de 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 langages/moteurs de création de modèles côté serveur tels que Django (Python), ERB/Haml (Ruby) et Smarty (PHP) existent depuis longtemps. Cependant, ces deux dernières années, nous avons vu une explosion de frameworks MVC. Tous sont légèrement différents, mais la plupart partagent un mécanisme commun pour afficher leur couche de présentation (ou vue): les modèles.

Soyons réalistes. Les modèles sont fantastiques. Allez-y, posez des questions. Même sa définition vous donne chaud :

"…ne doit pas être recréée à chaque fois…" Je ne sais pas pour vous, mais j'aime éviter le travail supplémentaire. Pourquoi la plate-forme Web ne propose-t-elle pas de prise en charge native pour un élément qui intéresse clairement les développeurs ?

La spécification des modèles HTML du 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, qui ne sont pas utilisés lors du chargement de la page, mais qui peuvent être instanciés plus tard au moment de l'exécution. Pour citer Rafael Weinstein :

Il s'agit d'un espace où placer un gros tas de code HTML que vous ne voulez pas que le navigateur modifie du tout, pour quelque raison que ce soit.

Rafael Weinstein (auteur de la fiche technique)

Détection de caractéristiques

Pour détecter <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 des "contenus de modèle", essentiellement des blocs inertes de DOM clonables. Considérez les modèles comme des éléments d'échafaudage que vous pouvez utiliser (et réutiliser) tout au long de la durée de vie de votre application.

Pour créer un contenu issu d'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

Encapsulant le contenu dans un <template>, nous obtenons quelques propriétés importantes.

  1. Son contenu est effectivement inerte jusqu'à son activation. En substance, votre balisage est un DOM masqué et ne s'affiche pas.

  2. Le contenu d'un modèle n'a aucun effet secondaire. Le script ne s'exécute pas, les images ne se chargent pas, l'audio ne se lit pas, etc., tant que le modèle n'est pas utilisé.

  3. Le contenu est considéré comme n'étant pas présent dans le document. L'utilisation de document.getElementById() ou querySelector() sur la page principale ne renvoie pas les nœuds enfants d'un modèle.

  4. 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é là où l'analyseur HTML interdit tous les enfants, sauf ceux du modèle de contenu. Il peut également être placé en tant qu'enfant de <table> ou <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 de procéder consiste à créer une copie approfondie de son .content à l'aide de document.importNode(). La propriété .content est une DocumentFragment en lecture seule contenant les détails 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 le modèle créé, 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. <script> ne s'exécute que lorsque le bouton est enfoncé, ce qui permet d'imprimer 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 DOM ombragé à 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 DOM fantôme est complexe, plus vous effectuez de concatenations de chaînes. Il n'est pas évolutif, les choses deviennent vite désordonnées et les bébés commencent à pleurer. C'est également grâce à cette approche que le XSS est né ! <template> à la rescousse.

Il est plus judicieux de 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 lors de l'utilisation de <template> dans la pratique :

  • Si vous utilisez modpagespeed, faites attention à ce bug. Les modèles qui définissent des <style scoped> intégrés peuvent être déplacés vers la section "head" avec les règles de réécriture CSS de PageSpeed.
  • Il n'existe aucun moyen de "prérendre" un modèle, ce qui signifie que vous ne pouvez pas précharger d'éléments, traiter du code JavaScript, télécharger du CSS initial, etc. Cela s'applique au serveur et au client. Un modèle ne s'affiche que lorsqu'il est mis en ligne.
  • Faites attention aux modèles imbriqués. Ils ne se comportent pas comme prévu. Exemple :

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    L'activation du modèle externe n'active pas les modèles internes. Autrement dit, les modèles imbriqués nécessitent que leurs enfants soient également activés manuellement.

Vers une norme

N'oublions pas d'où nous venons. Le chemin vers les modèles HTML basés sur des normes a été long. Au fil des ans, nous avons mis au point des astuces très pratiques pour créer des modèles réutilisables. Vous trouverez ci-dessous deux exemples courants que j'ai rencontrés. Je les inclut dans cet article à titre 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écapitulatif de cette technique :

  • À l'aide du DOM : le navigateur connaît le DOM. Il est bon à ce jeu. Nous pouvons facilement le cloner.
  • Aucun élément n'est affiché : l'ajout de hidden empêche l'affichage du bloc.
  • Pas inertes : même si notre contenu est masqué, une requête réseau est toujours effectuée pour l'image.
  • Style et thématisation difficiles : une page d'intégration doit ajouter le préfixe #mytemplate à toutes ses règles CSS afin de limiter le champ d'application des styles au modèle. Cette méthode est fragile et rien ne garantit que nous ne rencontrerons pas de futurs conflits de dénomination. Par exemple, nous obtenons une requête si la page de représentation vectorielle continue possède déjà un élément avec cet ID.

Méthode 2: Script de surcharge

Une autre technique consiste à surcharger <script> et à manipuler son contenu en tant que chaîne. John Resig a probablement été le premier à le montrer en 2008 avec son utilitaire de micro-modèle. Il en existe désormais de nombreux autres, y compris des 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écapitulatif de cette technique :

  • Aucun élément n'est affiché : le navigateur n'affiche pas ce bloc, car <script> est display:none par défaut.
  • Inerte : le navigateur n'analyse pas le contenu du script en tant que JavaScript, 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 au moment de l'exécution des données fournies par l'utilisateur peut facilement entraîner des failles XSS.

Conclusion

Vous souvenez-vous de l'époque où jQuery rendait le DOM extrêmement simple à utiliser ? querySelector()/querySelectorAll() a donc été ajouté à la plate-forme. Une victoire évidente, non ? Une bibliothèque a popularisé la récupération du DOM avec des sélecteurs CSS et des normes l'ont ensuite adoptée. Cela ne fonctionne pas toujours comme ça, mais j'adore ça quand c'est le cas.

Je pense que le cas de <template> est similaire. Il standardise la façon dont nous effectuons le typage côté client, mais surtout, il élimine le besoin de nos hacks de 2008. À mon avis, rendre l'ensemble du processus d'authoring Web plus rationnel, plus facile à gérer et plus complet est toujours une bonne chose.

Ressources supplémentaires