Nova tag de modelo de HTML

Padronização de modelos do lado do cliente

Introdução

O conceito de modelos não é novo no desenvolvimento da Web. Na verdade, linguagens/mecanismos de modelos do lado do servidor, como Django (Python), ERB/Haml (Ruby) e Smarty (PHP), já existem há muito tempo. No entanto, nos últimos anos, vimos uma explosão de frameworks de MVC. Todas elas são ligeiramente diferentes, mas a maioria tem uma mecânica comum para renderizar a camada de apresentação (também conhecida como visualização da): modelos.

Sejamos honestos. Os modelos são fantásticos. Vá em frente, faça perguntas. Até mesmo a definição dele faz você se sentir aquecido e confortável:

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

A especificação dos modelos HTML WhatWG é a resposta. Ela 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 da página, mas podem ser instanciados posteriormente no tempo de execução. Citando Rafael Weinstein:

Elas servem para colocar um monte de HTMLs com o qual você não quer que o navegador mexa... por nenhum motivo.

Rafael Weinstein (autor das especificações)

Detecção de recursos

Para detectar <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 conteúdo do modelo

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

Para criar um conteúdo de modelo, declare algumas marcações e envolva-as no elemento <template>:

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

Os pilares

Unir o conteúdo em um <template> nos oferece algumas propriedades importantes.

  1. O conteúdo dele fica inerte até ser ativado. Basicamente, sua marcação é ocultada do DOM 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 carregam, o áudio não toca até que o modelo seja usado.

  3. O conteúdo é considerado fora do documento. O uso de document.getElementById() ou querySelector() na página principal não retornará 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 HTML não permite, todos os filhos, exceto o 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>

Ativando um modelo

Para usar um modelo, você precisa ativá-lo. Caso contrário, o conteúdo nunca será renderizado. A maneira mais simples de fazer isso é criando uma cópia detalhada do .content usando document.importNode(). A propriedade .content é um DocumentFragment somente leitura que contém o elemento 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 copiar 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 inert

Esse exemplo demonstra a inércia do conteúdo do modelo. O <script> só é executado quando o botão é pressionado, indicando 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 o Shadow DOM a partir de 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 dessa abordagem é que, quanto mais complexo o Shadow DOM fica, mais concatenação de strings você está fazendo. Não dimensiona, as coisas ficam desordenadas rápido e bebês começam a chorar. Essa também é a origem do XSS. <template> ao resgate.

Algo mais sensato seria trabalhar com o DOM diretamente, anexando o conteúdo do modelo a uma raiz paralela:

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

pegadinhas

Aqui estão alguns problemas que encontrei ao usar o <template>:

  • Se você estiver usando modpagespeed, tenha cuidado com esse bug. Modelos que definem <style scoped> inline, muitos deles movidos para o cabeçalho com as regras de reescrita de CSS do PageSpeed.
  • Não há como "pré-renderizar" um modelo, o que significa que não é possível pré-carregar recursos, processar JS, fazer o download do CSS inicial etc. Isso vale para o servidor e o cliente. Um modelo só é renderizado quando é publicado.
  • Tenha cuidado com modelos aninhados. Eles não se comportam como esperado. Exemplo:

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

    Ativar o modelo externo não ativará 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 vamos esquecer de onde viemos. O caminho para modelos HTML baseados em padrões é longo. Ao longo dos anos, desenvolvemos alguns truques inteligentes para criar modelos reutilizáveis. Abaixo estão dois problemas comuns que encontrei. Elas estão neste artigo para fins de comparação.

Método 1: DOM fora da tela

Uma abordagem que as pessoas têm usado 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, há uma série de desvantagens. Resumo dessa técnica:

  • Usando o DOM: o navegador conhece o DOM. Ele é bom nisso. É fácil cloná-lo.
  • Nada foi renderizado: adicionar hidden impede que o bloco seja mostrado.
  • Not inert: mesmo que nosso conteúdo esteja oculto, uma solicitação de rede ainda será feita para a imagem.
  • Estilo e temas problemáticos: uma página de incorporação precisa prefixar todas as regras de CSS com #mytemplate para definir o escopo dos estilos até o modelo. Isso é frágil e não há garantias de que não encontraremos futuros conflitos de nomes. Por exemplo, ficamos com problemas se a página de incorporação já tiver um elemento com esse ID.

Método 2: script de sobrecarga

Outra técnica é sobrecarregar <script> e manipular o conteúdo dele como uma string. John Resig provavelmente foi o primeiro a mostrar isso em 2008 com o utilitário Micro Modeling. Agora há muitos outros usuários, incluindo algumas crianças novas no bloco, como handlebars.js.

Exemplo:

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

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 para algo diferente de "text/javascript".
  • Problemas de segurança: incentiva o uso de .innerHTML. A análise de strings no tempo de execução de dados fornecidos pelo usuário pode facilmente levar a vulnerabilidades XSS.

Conclusão

Lembra quando o jQuery simplificou o trabalho com o DOM? O resultado foi a adição de querySelector()/querySelectorAll() à plataforma. Vitória óbvia, certo? Uma biblioteca conhecida por buscar o DOM com seletores e padrões de CSS o adotou depois. Nem sempre funciona assim, mas eu amo quando isso funciona.

Acho que <template> é um caso semelhante. Ela padroniza a maneira como fazemos modelos do lado do cliente, mas, mais importante, elimina a necessidade das nossas invasões de 2008. Tornar todo o processo de criação na Web mais seguro, sustentável e mais completo é sempre uma coisa boa no meu livro.

Outros recursos