Como padronizar a criação de modelos do lado do cliente
Introdução
O conceito de modelos não é novo para o 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é a definição faz você se sentir aquecido e aconchegado:
"…não precisa ser recriado 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 criação de modelos do lado do cliente. Os modelos permitem que você declare fragmentos de marcação que são analisados como HTML, não são usados no carregamento da página, mas podem ser instanciados
mais tarde no ambiente de execução. Citando Rafael Weinstein:
Eles são um lugar para colocar um grande bloco de HTML que você não quer que o navegador mexa de forma alguma.
Rafael Weinstein (autor de 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 HTML <template>
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 scaffolding 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.
O conteúdo é inerte até ser ativado. Essencialmente, 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 não está no documento. O uso de
document.getElementById()
ouquerySelector()
na página principal não vai retornar 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 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>
Como ativar 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 é 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 criar 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 inativo
Este exemplo demonstra a inércia do conteúdo do modelo. O <script>
só
é executado quando o botão é pressionado, marcando 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 DOM shadow 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 DOM da sombra fica,
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.
Uma abordagem mais sensata seria trabalhar com o DOM diretamente 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>
Problemas
Confira alguns problemas que encontrei ao usar <template>
:
- Se você estiver usando o modpagespeed, tenha cuidado
com esse bug. Modelos
que definem
<style scoped>
inline podem ser movidos para o cabeçalho com as regras de reescrita de CSS do PageSpeed. - Não há como "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 único momento em que um modelo é renderizado é quando ele é ativado.
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 essas informações 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, 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 mostrado. - 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 vamos encontrar conflitos de nomenclatura no futuro. Por exemplo, se a página de incorporação já tiver um elemento com esse ID, o serviço será interrompido.
Método 2: sobrecarga de script
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 da 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? Uma biblioteca popularizou a busca de DOM
com os seletores e padrões CSS. Nem sempre funciona assim, mas adoro quando funciona.
Acho que <template>
é um caso semelhante. Ele padroniza a maneira como fazemos a criação de modelos
do lado do cliente, mas, mais importante, elimina a necessidade de nossos hacks de 2008.
Para mim, é sempre bom tornar todo o processo de criação da Web mais confiável, mais fácil de manter e com mais
recursos.
Outros recursos
- Especificação do WhatWG (em inglês)
- Introdução aos componentes da Web
- <web>components</web> (vídeo): uma apresentação fantásticamente abrangente.