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.
O conteúdo é inerte até ser ativado. Basicamente, sua marcação é um DOM oculto e não é renderizada.
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.
O conteúdo é considerado como não presente no documento. O uso de
document.getElementById()ouquerySelector()na página principal não retorna nós filhos de um modelo.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
hiddenimpede 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
#mytemplatepara 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:nonepor 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
- Especificação do WhatWG
- Introdução aos componentes da Web
- <web>components</web> (vídeo): uma apresentação incrivelmente abrangente feita por mim.