Nuovo tag modello HTML

Standardizzazione dei modelli lato client

Introduzione

Il concetto di templating non è nuovo per lo sviluppo web. Infatti, esistono da molto tempo linguaggi/motori di creazione di modelli lato server come Django (Python), ERB/Haml (Ruby) e Smarty (PHP). Negli ultimi due anni, tuttavia, abbiamo visto un'esplosione di framework MVC. Sono tutti leggermente diversi, ma la maggior parte condivide una stessa modalità di rendering del livello di presentazione (ovvero della visualizzazione): i modelli.

Ammettiamolo. I modelli sono fantastici. Vai, chiedi in giro. Anche la sua definizione ti fa sentire caldo e avvolgente:

"…non deve essere ricreato ogni volta…" Non so tu, ma amo evitare il lavoro extra. Perché allora la piattaforma web non supporta in modo nativo qualcosa che interessa chiaramente agli sviluppatori?

La specifica dei modelli HTML di WhatWG è la risposta. Definisce un nuovo elemento <template> che descrive un approccio standard basato su DOM per la creazione di modelli lato client. I modelli ti consentono di dichiarare frammenti di markup che vengono analizzati come HTML, non vengono utilizzati al caricamento della pagina, ma possono essere istigati più tardi in fase di esecuzione. Citando Rafael Weinstein:

Sono un luogo in cui inserire una grande quantità di codice HTML che non vuoi che il browser modifichi per nessun motivo.

Rafael Weinstein (autore della scheda)

Rilevamento di elementi

Per rilevare la funzionalità <template>, crea l'elemento DOM e verifica che esista la proprietà .content:

function supportsTemplate() {
    return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
    // Good to go!
} else {
    // Use old templating techniques or libraries.
}

Dichiarazione dei contenuti del modello

L'elemento HTML <template> rappresenta un modello nel markup. Contiene "contenuti del modello", ovvero blocchi inattivi di DOM clonabile. Pensa ai modelli come a pezzi di impalcatura che puoi utilizzare (e riutilizzare) per tutta la durata della tua app.

Per creare contenuti basati su modelli, dichiara del markup e racchiudilo nell'elemento <template>:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

I pilastri

L'inserimento dei contenuti in un <template> ci fornisce alcune proprietà importanti.

  1. I contenuti sono effettivamente inattivi fino all'attivazione. In sostanza, il markup è DOM nascosto e non viene visualizzato.

  2. Tutti i contenuti all'interno di un modello non avranno effetti collaterali. Lo script non viene eseguito, le immagini non vengono caricate, l'audio non viene riprodotto,…fino a quando non viene utilizzato il modello.

  3. I contenuti non sono considerati presenti nel documento. L'utilizzo di document.getElementById() o querySelector() nella pagina principale non restituisce i nodi secondari di un modello.

  4. I modelli possono essere posizionati ovunque all'interno di <head>, <body> o <frameset> e possono contenere qualsiasi tipo di contenuto consentito in questi elementi. Tieni presente che "ovunque" significa che <template> può essere utilizzato in sicurezza in posizioni non consentite dal parser HTML... tutti ad eccezione del modello dei contenuti. Può anche essere inserito come elemento secondario di <table> o <select>:

<table>
  <tr>
    <template id="cells-to-repeat">
      <td>some content</td>
    </template>
  </tr>
</table>

Attivare un modello

Per utilizzare un modello, devi attivarlo. In caso contrario, i relativi contenuti non verranno mai visualizzati. Il modo più semplice per farlo è creare una copia approfondita del relativo .content utilizzando document.importNode(). La proprietà .content è un DocumentFragment di sola lettura che contiene i dati del modello.

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);

Dopo aver stampato un modello, i relativi contenuti "vengono pubblicati". In questo esempio specifico, i contenuti vengono clonati, viene effettuata la richiesta di immagine e viene visualizzato il markup finale.

Demo

Esempio: script inattivo

Questo esempio dimostra l'inerzia dei contenuti del modello. <script> viene eseguito solo quando viene premuto il pulsante, eliminando così il modello.

<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>

Esempio: creazione di DOM ombra da un modello

La maggior parte degli utenti collega Shadow DOM a un host impostando una stringa di markup su .innerHTML:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.innerHTML = '<span>Host node</span>';
</script>

Il problema di questo approccio è che più il DOM ombra diventa complesso, più concatenazioni di stringhe vengono eseguite. Non è scalabile, le cose si complicano rapidamente e i bambini iniziano a piangere. È con questo approccio che è nato l'XSS. <template> accorre in soccorso.

Una cosa più sana sarebbe quella di lavorare direttamente con il DOM aggiungendo i contenuti del modello alla radice shadow:

<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>

Gotcha

Ecco alcuni problemi che ho riscontrato durante l'utilizzo di <template> in produzione:

  • Se utilizzi modpagespeed, fai attenzione a questo bug. I modelli che definiscono <style scoped> in linea possono essere spostati in head con le regole di riscrittura CSS di PageSpeed.
  • Non è possibile "prerenderizzare" un modello, il che significa che non puoi precaricare gli asset, elaborare JS, scaricare il CSS iniziale e così via. Questo vale sia per il server che per il client. Il rendering di un modello viene eseguito soltanto quando viene pubblicato.
  • Fai attenzione ai modelli nidificati. Non si comportano come ci si potrebbe aspettare. Ad esempio:

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

    L'attivazione del modello esterno non attiverà i modelli interni. In altre parole, i modelli nidificati richiedono che anche i relativi elementi secondari vengano attivati manualmente.

Il percorso verso uno standard

Non dimentichiamo da dove veniamo. Il percorso verso i modelli HTML basati su standard è stato lungo. Nel corso degli anni abbiamo ideato alcuni trucchi piuttosto intelligenti per creare modelli riutilizzabili. Di seguito sono riportati due casi comuni che ho riscontrato. Li includo in questo articolo per fare un confronto.

Metodo 1: DOM offscreen

Un approccio utilizzato da molto tempo consiste nel creare un DOM "offscreen" e nasconderlo alla visualizzazione utilizzando l'attributo hidden o display:none.

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Sebbene questa tecnica funzioni, presenta una serie di svantaggi. Riepilogo della tecnica:

  • Utilizzando DOM: il browser conosce il DOM. È brava. Possiamo clonarlo facilmente.
  • Non viene visualizzato nulla: l'aggiunta di hidden impedisce la visualizzazione del blocco.
  • Non inerti: anche se i nostri contenuti sono nascosti, viene comunque effettuata una richiesta di rete per l'immagine.
  • Difficoltà con stili e temi: una pagina di incorporamento deve anteporre a tutte le sue regole CSS il prefisso #mytemplate per limitare l'ambito degli stili al modello. Si tratta del problema e non vi è alcuna garanzia che in futuro non riscontreremo collisioni di nomi. Ad esempio, non possiamo procedere se la pagina di incorporamento contiene già un elemento con quell'ID.

Metodo 2: script di sovraccarico

Un'altra tecnica è il sovraccarico di <script> e la manipolazione dei relativi contenuti come stringa. John Resig è stato probabilmente il primo a dimostrare questo aspetto nel 2008 con la sua utility per la creazione di micro modelli. Ora ce ne sono molti altri, inclusi alcuni nuovi bambini nel blocco come handlebars.js.

Ad esempio:

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

Riepilogo della tecnica:

  • Non viene visualizzato nulla: il browser non esegue il rendering di questo blocco perché <script> è display:none per impostazione predefinita.
  • Inert: il browser non analizza il contenuto dello script come JS perché il suo tipo è impostato su un valore diverso da "text/JavaScript".
  • Problemi di sicurezza: incoraggia l'uso di .innerHTML. L'analisi di stringhe a runtime dei dati forniti dall'utente può facilmente portare a vulnerabilità XSS.

Conclusione

Ricordi quando jQuery rendeva molto semplice lavorare con il DOM? Il risultato è stato l'aggiunta di querySelector()/querySelectorAll() alla piattaforma. Una vittoria netta, giusto? Una libreria ha reso popolare il recupero del DOM con selettori CSS e standard che in seguito lo hanno adottato. Non funziona sempre così, ma adoro quando succede.

Penso che <template> sia una richiesta simile. Standardizza il modo in cui gestiamo la creazione di modelli lato client, ma, cosa più importante, elimina la necessità dei nostri hack del 2008. Per me, rendere l'intera procedura di creazione di contenuti web più sana, più gestibile e più completa è sempre un bene.

Risorse aggiuntive