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/motores 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, observamos uma explosão de frameworks MVC. Todos eles são um pouco diferentes, mas a maioria compartilha uma mecânica comum para renderizar a camada de apresentação (também conhecida como visualização): modelos.

Vamos ser honestos. Os modelos são fantásticos. Vá em frente, pergunte. Até mesmo a definição faz você se sentir aquecido e aconchegante:

"…não precisa ser recriada toda vez…" 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 na carga da página, mas podem ser instanciados mais tarde no momento da execução. Citando Rafael Weinstein:

São lugares para colocar um grande embrulho de HTML com o qual você não quer que o navegador manipule... por qualquer 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 o conteúdo do modelo

O elemento <template> HTML representa um modelo na sua marcação. Ele contém "conteúdos de modelo", ou seja, fragmentos inativos de DOM clonáveis. Pense nos modelos como peças de andaime que podem ser usadas (e reutilizadas) durante todo o ciclo de vida do app.

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

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

Os pilares

O agrupamento de conteúdo em um <template> nos dá algumas propriedades importantes.

  1. O conteúdo fica efetivamente inerte até ser ativado. Essencialmente, 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 não está no documento. O uso de document.getElementById() ou querySelector() na página principal não vai 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 lugares que o analisador de HTML não permite, exceto 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>

Ativar um modelo

Para usar um modelo, você precisa ativá-lo. Caso contrário, o conteúdo não 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 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 estampar um modelo, o conteúdo dele será 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 inativo

Este exemplo demonstra a inércia do conteúdo do modelo. O <script> só é executado quando o botão é pressionado, carimbando 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 sombra usando um modelo

A maioria das pessoas anexa o Shadow DOM a um host definindo uma string de markup para .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, mais concatenações de string você faz. Ele não é escalonável, as coisas ficam bagunçadas rapidamente e os bebês começam a chorar. Essa abordagem também é como o XSS nasceu em primeiro lugar. <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 pegadinhas que eu encontrei ao usar o <template>:

  • Se você estiver usando modpagespeed, tenha cuidado com esse bug. Os modelos que definem <style scoped> in-line, muitos são movidos para o início com as regras de reescrita de CSS do PageSpeed.
  • Não há como "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. O modelo só é renderizado quando é publicado.
  • Tenha cuidado com 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 se esqueça de onde viemos. O caminho para modelos HTML baseados em padrões é longo. Ao longo dos anos, criamos alguns truques bem inteligentes para criar modelos reutilizáveis. Confira abaixo dois exemplos comuns que encontrei. Estou incluindo elas neste artigo para fins de 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, há várias desvantagens. Resumo da técnica:

  • Usar o DOM: o navegador conhece o DOM. Ele é bom nisso. Podemos cloná-lo facilmente.
  • Nada é renderizado: adicionar hidden impede que o bloco seja exibido.
  • Não é inerte: mesmo que o conteúdo esteja oculto, a imagem ainda recebe uma solicitação de rede.
  • Estilo e tema problemáticos: uma página de incorporação precisa prefixar todas as regras de CSS com #mytemplate para limitar os estilos ao modelo. Isso é frágil e não há garantias de que não haverá conflitos de nomenclatura futuros. Por exemplo, se a página de incorporação já tiver um elemento com esse ID, o serviço será interrompido.

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 seu utilitário Micro Templating. Agora há muitos outros, incluindo alguns novos, 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 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 simplificou o trabalho com DOM? O resultado foi a adição de querySelector()/querySelectorAll() à plataforma. Vitória óbvia, certo? Depois, uma biblioteca popularizou a busca de DOM com seletores e padrões de CSS. Nem sempre funciona assim, mas eu adoro quando funciona.

Acho que <template> é um caso semelhante. Ele padroniza a maneira como fazemos os modelos do lado do cliente, mas, mais importante, remove a necessidade das nossas hacks de 2008. Tornar todo o processo de criação da Web mais íntegro, mais sustentável e mais pleno é sempre uma coisa boa no meu livro.

Outros recursos