Nouvelle balise de modèle HTML

Normaliser des modèles côté client

Introduction

Le concept de création de modèles n'est pas nouveau pour 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. Toutefois, ces deux dernières années, nous avons vu une explosion des 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 met au chaud et au chaud:

« ... n'a pas besoin d'être recréé à chaque fois »... Je ne sais pas pour vous, mais j'aime éviter des tâches supplémentaires. Pourquoi la plate-forme Web manque-t-elle de compatibilité native pour un élément qui intéresse clairement les développeurs ?

La réponse est dans les spécifications des modèles HTML WhatsApp. 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. Ils ne sont pas utilisés au chargement de la page, mais peuvent être instanciés ultérieurement au moment de l'exécution. Pour reprendre la citation de Rafael Weinstein:

Ils permettent d'insérer une grande quantité de code HTML que vous ne voulez pas que le navigateur interfère pour une raison quelconque.

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 d'un 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 essentiellement des fragments inertes de DOM cloné. 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

L'encapsulation du contenu dans une <template> nous donne peu de propriétés importantes.

  1. Son contenu reste inerte jusqu'à ce qu'il soit activé. En gros, votre balisage est un DOM masqué et ne s'affiche pas.

  2. Le contenu d'un modèle n'aura aucun effet secondaire. Le script ne s'exécute pas, les images ne se chargent pas, le contenu audio n'est pas lu, et ce, jusqu'à ce que le modèle soit utilisé.

  3. Le contenu est considéré comme ne figurant pas dans le document. L'utilisation de document.getElementById() ou querySelector() sur la page principale ne renverra 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 comporter 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 de 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. Pour ce faire, le moyen le plus simple consiste à créer une copie profonde de son .content à l'aide de document.importNode(). La propriété .content est un élément DocumentFragment en lecture seule qui contient les informations 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 un modèle estampé, son contenu est mis en ligne. Dans cet exemple, le contenu est cloné, la demande 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 vous appuyez sur le bouton, ce qui permet d'embaser 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 un 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 est complexe, plus vous faites de concaténation de chaînes. Il n'évolue pas, les choses se compliquent rapidement et les bébés se mettent à pleurer. C'est aussi grâce à cette approche que le XSS est né ! <template> à la rescousse.

Il serait 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 problèmes que j'ai rencontrés lorsque j'utilise <template> dans la nature:

  • 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 au premier plan avec les règles de réécriture CSS de PageSpeed.
  • Il n'existe aucun moyen de "précharger" un modèle, ce qui signifie que vous ne pouvez pas précharger les éléments, traiter le code JS, télécharger le CSS initial, etc. Cela vaut pour le serveur et pour le 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'entraîne pas l'activation des modèles internes. Autrement dit, les modèles imbriqués nécessitent que leurs enfants soient également activés manuellement.

La voie vers une norme

N'oublions pas d'où nous venons. L'adoption des modèles HTML standards a été longue. Au fil des ans, nous avons inventé des astuces ingénieuses pour créer des modèles réutilisables. Voici deux cas courants que j'ai rencontrés. Vous les trouverez 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 le détail de cette technique:

  • Utilisation de DOM (le navigateur reconnaît le DOM) C'est bien. Nous pouvons facilement le cloner.
  • Rien ne s'affiche : l'ajout de hidden empêche le bloc de s'afficher.
  • Non inerte : même si notre contenu est masqué, une requête réseau est quand même effectuée pour l'image.
  • Personnalisation et style douloureux : une page d'intégration doit ajouter le préfixe #mytemplate à toutes ses règles CSS pour limiter les styles au modèle. Cette situation est fragile, et nous n'avons aucune garantie d'éviter de futurs conflits de noms. Par exemple, cette erreur est renvoyée si la page d'intégration comporte déjà un élément avec cet ID.

Méthode 2: surcharge du script

Une autre technique consiste à surcharger <script> et à manipuler son contenu en tant que chaîne. John Resig a probablement été le premier à présenter cela en 2008 avec son utilitaire Micro TEmplating. Il y en a beaucoup d'autres, y compris de nouveaux enfants dans le bloc, comme handlebars.js.

Exemple :

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

Voici le détail de cette technique:

  • Rien 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 JS, car son type est défini sur autre chose que "text/javascript".
  • Problèmes de sécurité : encourage l'utilisation de .innerHTML. L'analyse de chaîne au moment de l'exécution des données fournies par l'utilisateur peut facilement entraîner des failles XSS.

Conclusion

Vous vous souvenez quand jQuery a rendu l'utilisation du DOM très simple ? Résultat : querySelector()/querySelectorAll() ont été ajoutés à la plate-forme. Une victoire évidente, non ? Une bibliothèque a popularisé l'extraction du DOM avec des sélecteurs et des normes CSS par la suite. Ça ne fonctionne pas toujours de cette façon, mais j'adore quand c'est le cas.

Je pense que <template> est un cas similaire. Elle standardise la façon dont nous créons des modèles côté client, mais, plus important encore, elle élimine la nécessité d'avoir recours à nos hacks de 2008. Rendre l'ensemble du processus de création Web plus pertinent, plus facile à gérer et plus complet, est toujours une bonne chose dans mon livre.

Ressources supplémentaires