Nova tag de modelo do HTML

Padronização de modelos do lado do cliente

Introdução

O conceito de modelos não é novo no desenvolvimento web. Na verdade, as linguagens/mecanismos de modelos do lado do servidor, como Django (Python), ERB/Haml (Ruby) e Smarty (PHP), já existem há muito tempo. Nos últimos anos, no entanto, vimos uma explosão de frameworks MVC. Todos eles são um pouco diferentes, mas a maioria compartilha um mecanismo comum para renderizar a camada de apresentação (também conhecida como visualização): os modelos.

Vamos ser honestos. Os modelos são fantásticos. Pode perguntar por aí. Até a definição (link em inglês) dela faz você se sentir confortável e aconchegante:

"…não precisa ser recriado todas as vezes…" Não sei você, mas eu adoro evitar trabalho extra. Por que a plataforma da Web não tem suporte nativo para algo que os desenvolvedores claramente se importam?

A especificação de modelos HTML do WhatWG é a resposta. Ele define um novo elemento <template> que descreve uma abordagem padrão baseada em DOM para modelos do lado do cliente. Os modelos permitem declarar fragmentos de marcação que são analisados como HTML, não são usados no carregamento de página, mas podem ser instanciados posteriormente no ambiente de execução. Para citar Rafael Weinstein:

Eles são um lugar para colocar um grande bloco de HTML que você não quer que o navegador mexa de jeito nenhum, por qualquer motivo.

Rafael Weinstein (autor da especificação)

Detecção de recursos

Para detectar o recurso <template>, crie o elemento DOM e verifique se a propriedade .content existe:

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

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

Como declarar o conteúdo do modelo

O elemento HTML <template> representa um modelo na sua marcação. Ele contém "conteúdo de modelo", essencialmente partes inertes de DOM clonável. Pense nos modelos como partes de uma estrutura que você pode usar (e reutilizar) durante o ciclo de vida do app.

Para criar um conteúdo com modelo, declare uma marcação e envolva-a no elemento <template>:

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

Os pilares

O encapsulamento de conteúdo em um <template> oferece algumas propriedades importantes.

  1. O conteúdo é inerte até ser ativado. Basicamente, sua marcação é um DOM oculto e não é renderizada.

  2. O conteúdo de um modelo não tem efeitos colaterais. O script não é executado, as imagens não são carregadas, o áudio não é reproduzido… até que o modelo seja usado.

  3. O conteúdo é considerado como não presente no documento. O uso de document.getElementById() ou querySelector() na página principal não retorna nós filhos de um modelo.

  4. Os modelos podem ser colocados em qualquer lugar dentro de <head>, <body> ou <frameset> e podem conter qualquer tipo de conteúdo permitido nesses elementos. "Em qualquer lugar" significa que <template> pode ser usado com segurança em locais que o analisador de HTML não permite, exceto em filhos do modelo de conteúdo. Ele também pode ser colocado como filho de <table> ou <select>:

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

Como ativar um modelo

Para usar um modelo, é necessário ativá-lo. Caso contrário, o conteúdo nunca será renderizado. A maneira mais simples de fazer isso é criar uma cópia profunda do .content usando document.importNode(). A propriedade .content é um DocumentFragment somente leitura que contém o conteúdo do modelo.

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

Depois de carimbar um modelo, o conteúdo dele é publicado. Neste exemplo específico, o conteúdo é clonado, a solicitação de imagem é feita e a marcação final é renderizada.

Demonstrações

Exemplo: script inerte

Este exemplo demonstra a inércia do conteúdo do modelo. O <script> só é executado quando o botão é pressionado, eliminando o modelo.

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

Exemplo: como criar um DOM de sombra com base em um modelo

A maioria das pessoas anexa o Shadow DOM a um host definindo uma string de marcação como .innerHTML:

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

O problema com essa abordagem é que, quanto mais complexo o Shadow DOM fica, mais concatenação de strings você faz. Não é escalonável, as coisas ficam confusas rapidamente e os bebês começam a chorar. Essa abordagem também é como o XSS nasceu em primeiro lugar! <template> ao resgate.

Uma opção mais sensata seria trabalhar diretamente com o DOM, anexando o conteúdo do modelo a uma raiz 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>

Gotchas

Veja algumas pegadinhas que encontrei ao usar o <template> na prática:

  • Se você estiver usando o modpagespeed, tome cuidado com este bug. Os modelos que definem <style scoped> inline podem ser movidos para o cabeçalho com as regras de reescrita de CSS do PageSpeed.
  • Não é possível "pré-renderizar" um modelo, ou seja, não é possível pré-carregar recursos, processar JS, fazer o download do CSS inicial etc. Isso vale para o servidor e o cliente. A única vez que um modelo é renderizado é quando ele é publicado.
  • Cuidado com os modelos aninhados. Eles não se comportam como você espera. Exemplo:

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

    A ativação do modelo externo não ativa os modelos internos. Ou seja, os modelos aninhados exigem que os filhos também sejam ativados manualmente.

O caminho para um padrão

Não podemos esquecer de onde viemos. O caminho para os modelos HTML baseados em padrões tem sido longo. Ao longo dos anos, criamos alguns truques bem inteligentes para criar modelos reutilizáveis. Veja abaixo dois exemplos comuns que encontrei. Eles estão incluídos neste artigo para comparação.

Método 1: DOM fora da tela

Uma abordagem que as pessoas usam há muito tempo é criar um DOM "fora da tela" e ocultá-lo usando o atributo hidden ou display:none.

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

Embora essa técnica funcione, ela tem várias desvantagens. O resumo dessa técnica:

  • Usando o DOM: o navegador conhece o DOM. Ele é bom nisso. Podemos cloná-lo facilmente.
  • Nada é renderizado: adicionar hidden impede que o bloco seja mostrado.
  • Não inerte: mesmo que o conteúdo esteja oculto, uma solicitação de rede ainda é feita para a imagem.
  • Estilo e temas difíceis: uma página de incorporação precisa prefixar todas as regras de CSS com #mytemplate para definir o escopo dos estilos no modelo. Isso é frágil, e não há garantias de que não vamos encontrar conflitos de nomenclatura no futuro. Por exemplo, se a página de incorporação já tiver um elemento com esse ID, não vai funcionar.

Método 2: script de sobrecarga

Outra técnica é sobrecarregar <script> e manipular o conteúdo como uma string. John Resig provavelmente foi o primeiro a mostrar isso em 2008 com o utilitário de micro modelagem. Agora existem muitos outros, incluindo alguns novos, como o handlebars.js.

Exemplo:

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

O resumo dessa técnica:

  • Nada é renderizado: o navegador não renderiza esse bloco porque <script> é display:none por padrão.
  • Inert: o navegador não analisa o conteúdo do script como JS porque o tipo dele está definido como algo diferente de "text/javascript".
  • Problemas de segurança: incentiva o uso de .innerHTML. A análise de strings em tempo de execução de dados fornecidos pelo usuário pode facilmente levar a vulnerabilidades de XSS.

Conclusão

Lembra quando o jQuery facilitou muito o trabalho com o DOM? O resultado foi a adição de querySelector()/querySelectorAll() à plataforma. Uma vitória óbvia, certo? Uma biblioteca popularizou a busca de DOM com seletores de CSS, e os padrões adotaram isso depois. Nem sempre é assim, mas eu adoro quando acontece.

Acho que <template> é um caso semelhante. Ele padroniza a forma como fazemos modelos do lado do cliente, mas, mais importante, remove a necessidade de usar os hacks de 2008. Tornar todo o processo de criação na Web mais sensato, mais fácil de manter e mais completo é sempre uma boa ideia, na minha opinião.

Outros recursos