Nos artigos anteriores (Mini Projeto: Calculadora no Console, O que é o DOM e como o JavaScript interage com o HTML, Eventos: click, input, submit e muito mais) aprendemos a selecionar elementos existentes no HTML e reagir a eventos. Mas em aplicações reais, boa parte da interface não existe no HTML quando a página carrega — ela é construída pelo JavaScript em tempo de execução.
Pense em uma lista de tarefas onde você adiciona itens, um feed de posts que carrega conforme você rola, uma tabela que é preenchida com dados de uma API. Tudo isso é criado dinamicamente. Este artigo ensina exatamente como fazer isso.
createElement — criando elementos do zero
O método document.createElement() cria um novo elemento HTML na memória — ele ainda não aparece na página:
// Cria um elemento <p> na memória
const paragrafo = document.createElement("p");
// Cria outros tipos de elementos
const titulo = document.createElement("h2");
const botao = document.createElement("button");
const imagem = document.createElement("img");
const input = document.createElement("input");
const lista = document.createElement("ul");
const item = document.createElement("li");
Criar o elemento não é suficiente — você precisa configurá-lo e depois inserir na página.
Configurando o elemento antes de inserir
const card = document.createElement("div");
// Conteúdo
card.textContent = "Meu primeiro card criado com JavaScript!";
// Classes
card.classList.add("card", "destaque");
// Atributos
card.setAttribute("id", "card-principal");
card.setAttribute("data-tipo", "informativo");
// Estilos inline (quando necessário)
card.style.padding = "1rem";
card.style.border = "1px solid #ddd";
card.style.borderRadius = "8px";
appendChild — inserindo na página
Após criar e configurar o elemento, use appendChild para adicioná-lo como último filho de um elemento pai:
const container = document.querySelector("#container");
const paragrafo = document.createElement("p");
paragrafo.textContent = "Este parágrafo foi criado com JavaScript.";
// Insere no final do container
container.appendChild(paragrafo);
Métodos modernos de inserção
O appendChild é clássico mas limitado. Os métodos modernos são mais flexíveis:
const lista = document.querySelector("#lista");
const novoItem = document.createElement("li");
novoItem.textContent = "Novo item";
// Insere como último filho (igual ao appendChild)
lista.append(novoItem);
// Insere como primeiro filho
lista.prepend(novoItem);
// Insere ANTES do elemento referenciado
const terceiroItem = document.querySelector("#item-3");
terceiroItem.before(novoItem);
// Insere DEPOIS do elemento referenciado
terceiroItem.after(novoItem);
// append também aceita strings diretamente
lista.append("Texto simples sem criar elemento");
// append aceita múltiplos elementos de uma vez
const item1 = document.createElement("li");
const item2 = document.createElement("li");
item1.textContent = "Item A";
item2.textContent = "Item B";
lista.append(item1, item2);
insertAdjacentHTML — inserindo HTML em string
Quando você precisa inserir HTML mais complexo rapidamente:
const container = document.querySelector("#container");
// Posições possíveis:
// "beforebegin" — antes do elemento
// "afterbegin" — dentro, antes do primeiro filho
// "beforeend" — dentro, depois do último filho
// "afterend" — depois do elemento
container.insertAdjacentHTML("beforeend", `
<div class="card">
<h3>Título do Card</h3>
<p>Descrição do card inserida via insertAdjacentHTML.</p>
<button class="btn">Saiba mais</button>
</div>
`);
Atenção: nunca use insertAdjacentHTML ou innerHTML com dados vindos diretamente do usuário. Use createElement + textContent nesses casos para evitar ataques XSS.
Removendo elementos
const item = document.querySelector("#item-remover");
// Forma moderna — o elemento remove a si mesmo
item.remove();
// Forma clássica — o pai remove o filho
const pai = item.parentElement;
pai.removeChild(item);
Limpando o conteúdo de um elemento
const lista = document.querySelector("#lista");
// ✅ Forma eficiente
lista.innerHTML = "";
// ✅ Alternativa com loop (útil quando precisa remover com lógica)
while (lista.firstChild) {
lista.removeChild(lista.firstChild);
}
cloneNode — clonando elementos
const card = document.querySelector(".card-modelo");
// Clona apenas o elemento (sem filhos)
const clonaRaso = card.cloneNode(false);
// Clona o elemento e todos os filhos — deep clone
const clonaCompleto = card.cloneNode(true);
document.querySelector("#container").appendChild(clonaCompleto);
DocumentFragment — performance ao inserir muitos elementos
Quando você precisa inserir muitos elementos de uma vez, usar DocumentFragment é muito mais eficiente — você monta tudo na memória e insere de uma só vez, causando apenas um reflow (recálculo do layout):
const lista = document.querySelector("#lista");
const fragment = document.createDocumentFragment();
// Cria 100 itens na memória — zero operações no DOM real
for (let i = 1; i <= 100; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
// Uma única operação no DOM real
lista.appendChild(fragment);
Exemplo completo — To-Do List dinâmica
Vamos construir uma lista de tarefas completa com criação, conclusão e remoção de itens:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>To-Do List</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f2f5;
display: flex;
justify-content: center;
padding: 2rem 1rem;
}
.app {
background: white;
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 480px;
box-shadow: 0 4px 24px rgba(0,0,0,.08);
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #1a1a2e;
}
.formulario {
display: flex;
gap: .5rem;
margin-bottom: 1.5rem;
}
.formulario input {
flex: 1;
padding: .65rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color .2s;
}
.formulario input:focus {
outline: none;
border-color: #5c6bc0;
}
.formulario button {
padding: .65rem 1.2rem;
background: #5c6bc0;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background .2s;
}
.formulario button:hover { background: #3949ab; }
.info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: .85rem;
color: #666;
margin-bottom: 1rem;
}
.btn-limpar {
background: none;
border: none;
color: #e53935;
cursor: pointer;
font-size: .85rem;
text-decoration: underline;
}
.lista { list-style: none; }
.tarefa {
display: flex;
align-items: center;
gap: .75rem;
padding: .75rem 1rem;
border-radius: 8px;
margin-bottom: .5rem;
background: #f8f9ff;
border: 1px solid #e8eaf6;
transition: opacity .3s;
animation: entrar .2s ease;
}
@keyframes entrar {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.tarefa.concluida { opacity: .5; }
.tarefa.concluida .texto {
text-decoration: line-through;
color: #999;
}
.checkbox {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #5c6bc0;
}
.texto { flex: 1; font-size: .95rem; }
.btn-remover {
background: none;
border: none;
color: #ccc;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
transition: color .2s;
padding: 0 .25rem;
}
.btn-remover:hover { color: #e53935; }
.vazio {
text-align: center;
color: #bbb;
padding: 2rem 0;
font-size: .95rem;
}
</style>
</head>
<body>
<div class="app">
<h1>📝 Minhas Tarefas</h1>
<div class="formulario">
<input type="text" id="input-tarefa" placeholder="Nova tarefa..." maxlength="80">
<button id="btn-adicionar">Adicionar</button>
</div>
<div class="info">
<span id="contador">0 tarefas</span>
<button class="btn-limpar" id="btn-limpar">Limpar concluídas</button>
</div>
<ul class="lista" id="lista"></ul>
</div>
<script>
// Referências
const inputTarefa = document.querySelector("#input-tarefa");
const btnAdicionar = document.querySelector("#btn-adicionar");
const btnLimpar = document.querySelector("#btn-limpar");
const lista = document.querySelector("#lista");
const contador = document.querySelector("#contador");
// Estado
let tarefas = [];
let proximoId = 1;
// ── Funções de renderização ──────────────────────
function criarElementoTarefa(tarefa) {
const li = document.createElement("li");
li.classList.add("tarefa");
if (tarefa.concluida) li.classList.add("concluida");
li.dataset.id = tarefa.id;
// Checkbox
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.classList.add("checkbox");
checkbox.checked = tarefa.concluida;
checkbox.addEventListener("change", () => alternarTarefa(tarefa.id));
// Texto
const span = document.createElement("span");
span.classList.add("texto");
span.textContent = tarefa.texto;
// Botão remover
const btnRemover = document.createElement("button");
btnRemover.classList.add("btn-remover");
btnRemover.textContent = "×";
btnRemover.title = "Remover tarefa";
btnRemover.addEventListener("click", () => removerTarefa(tarefa.id));
li.append(checkbox, span, btnRemover);
return li;
}
function renderizar() {
lista.innerHTML = "";
if (tarefas.length === 0) {
const vazio = document.createElement("li");
vazio.classList.add("vazio");
vazio.textContent = "Nenhuma tarefa ainda. Adicione uma acima!";
lista.appendChild(vazio);
atualizarContador();
return;
}
// Usa DocumentFragment para performance
const fragment = document.createDocumentFragment();
tarefas.forEach(tarefa => {
fragment.appendChild(criarElementoTarefa(tarefa));
});
lista.appendChild(fragment);
atualizarContador();
}
function atualizarContador() {
const total = tarefas.length;
const concluidas = tarefas.filter(t => t.concluida).length;
const pendentes = total - concluidas;
contador.textContent = total === 0
? "Nenhuma tarefa"
: `${pendentes} pendente(s) · ${concluidas} concluída(s)`;
}
// ── Ações ────────────────────────────────────────
function adicionarTarefa() {
const texto = inputTarefa.value.trim();
if (!texto) {
inputTarefa.focus();
inputTarefa.style.borderColor = "#e53935";
setTimeout(() => {
inputTarefa.style.borderColor = "";
}, 1000);
return;
}
const novaTarefa = {
id: proximoId++,
texto,
concluida: false,
};
tarefas.push(novaTarefa);
inputTarefa.value = "";
inputTarefa.focus();
renderizar();
}
function alternarTarefa(id) {
tarefas = tarefas.map(t =>
t.id === id ? { ...t, concluida: !t.concluida } : t
);
renderizar();
}
function removerTarefa(id) {
// Animação de saída antes de remover
const elemento = lista.querySelector(`[data-id="${id}"]`);
if (elemento) {
elemento.style.transition = "opacity .2s, transform .2s";
elemento.style.opacity = "0";
elemento.style.transform = "translateX(20px)";
setTimeout(() => {
tarefas = tarefas.filter(t => t.id !== id);
renderizar();
}, 200);
}
}
function limparConcluidas() {
tarefas = tarefas.filter(t => !t.concluida);
renderizar();
}
// ── Eventos ──────────────────────────────────────
btnAdicionar.addEventListener("click", adicionarTarefa);
inputTarefa.addEventListener("keydown", (e) => {
if (e.key === "Enter") adicionarTarefa();
});
btnLimpar.addEventListener("click", limparConcluidas);
// ── Inicialização ─────────────────────────────────
// Tarefas de exemplo para começar
const tarefasIniciais = [
"Estudar criação de elementos no DOM",
"Praticar criação de elementos no DOM",
"Construir um projeto próprio",
];
tarefasIniciais.forEach(texto => {
tarefas.push({ id: proximoId++, texto, concluida: false });
});
renderizar();
inputTarefa.focus();
</script>
</body>
</html>
O que este projeto exercitou
| Técnica | Onde foi usada |
|---|---|
createElement |
Criação de cada elemento da tarefa |
appendChild / append |
Montagem do card da tarefa |
remove + animação |
Remoção suave de tarefas |
DocumentFragment |
Renderização performática da lista |
dataset |
Armazenar o id da tarefa no elemento |
classList.add |
Marcar a tarefa como concluída ao montar o item |
innerHTML = "" |
Limpar lista antes de re-renderizar |
| Um listener por elemento | Cada tarefa recebe os seus, recriados a cada render — o oposto da delegação do artigo anterior |
Tarefa para você
Expanda a To-Do List com:
- Prioridade — adicione um seletor
<select>com opções baixa, média e alta. Exiba uma bolinha colorida ao lado de cada tarefa - Edição — ao dar duplo clique no texto, transforme-o em um
<input>editável. Ao pressionarEnterou perder o foco, salve a edição - Filtro — adicione botões para mostrar: Todas / Pendentes / Concluídas
Ver solução — prioridade, edição inline e filtro sobre a To-Do do artigo
// Continuação da To-Do List do artigo: mesmas classes (.tarefa, .texto,
// .btn-remover) e o mesmo par tarefas[] / renderizar().
// ---- index.html
// <div class="formulario">
// <input type="text" id="input-tarefa" placeholder="Nova tarefa...">
// <select id="select-prioridade">
// <option value="baixa">Baixa</option>
// <option value="media" selected>Média</option>
// <option value="alta">Alta</option>
// </select>
// <button id="btn-adicionar">Adicionar</button>
// </div>
// <div class="filtros">
// <button class="filtro ativo" data-filtro="todas">Todas</button>
// <button class="filtro" data-filtro="pendentes">Pendentes</button>
// <button class="filtro" data-filtro="concluidas">Concluídas</button>
// </div>
//
// <style>
// .bolinha { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
// .bolinha.baixa { background: #66bb6a; }
// .bolinha.media { background: #ffa726; }
// .bolinha.alta { background: #ef5350; }
// .filtro.ativo { background: #5c6bc0; color: #fff; }
// .texto[contenteditable="true"] { background: #fff; outline: 2px solid #5c6bc0;
// border-radius: 4px; padding: 0 .25rem; }
// </style>
// ---- app.js
const selectPrioridade = document.querySelector("#select-prioridade");
const botoesFiltro = document.querySelectorAll(".filtro");
let filtroAtual = "todas";
const PRIORIDADES = {
baixa: { rotulo: "Baixa", peso: 1 },
media: { rotulo: "Média", peso: 2 },
alta: { rotulo: "Alta", peso: 3 },
};
// ---------------------------------------------------------------
// 1 — prioridade
// ---------------------------------------------------------------
function adicionarTarefa() {
const texto = inputTarefa.value.trim();
if (texto === "") return;
tarefas.push({
id: proximoId++,
texto,
concluida: false,
prioridade: selectPrioridade.value, // "baixa" | "media" | "alta"
});
inputTarefa.value = "";
renderizar();
}
function criarBolinha(prioridade) {
const bolinha = document.createElement("span");
// Duas classes: uma para o formato, outra para a cor. O CSS decide
// qual é cada cor — o JS não conhece hexadecimal nenhum.
bolinha.classList.add("bolinha", prioridade);
bolinha.title = `Prioridade ${PRIORIDADES[prioridade].rotulo.toLowerCase()}`;
return bolinha;
}
// ---------------------------------------------------------------
// 2 — edição por duplo clique
// ---------------------------------------------------------------
function habilitarEdicao(span, tarefa) {
span.addEventListener("dblclick", () => {
const textoOriginal = tarefa.texto;
span.contentEditable = "true";
span.focus();
// Cursor no fim do texto, em vez de no começo:
const selecao = window.getSelection();
const alcance = document.createRange();
alcance.selectNodeContents(span);
alcance.collapse(false);
selecao.removeAllRanges();
selecao.addRange(alcance);
function encerrar(salvar) {
span.contentEditable = "false";
if (salvar) {
const novo = span.textContent.trim();
// Texto vazio não pode virar tarefa fantasma: volta ao anterior.
tarefa.texto = novo === "" ? textoOriginal : novo;
}
span.textContent = tarefa.texto;
renderizar();
}
span.addEventListener("keydown", (evento) => {
if (evento.key === "Enter") {
evento.preventDefault(); // sem isso, o Enter insere quebra de linha
encerrar(true);
}
if (evento.key === "Escape") {
encerrar(false); // Escape descarta, e o texto original volta
}
});
// blur cobre "clicou fora". Vem depois do Enter, mas aí o
// contentEditable já é "false" e o encerrar() só repete o valor.
span.addEventListener("blur", () => encerrar(true), { once: true });
});
}
// ---------------------------------------------------------------
// 3 — filtros
// ---------------------------------------------------------------
const FILTROS = {
todas: () => true,
pendentes: (tarefa) => !tarefa.concluida,
concluidas: (tarefa) => tarefa.concluida,
};
botoesFiltro.forEach((botao) => {
botao.addEventListener("click", () => {
filtroAtual = botao.dataset.filtro;
botoesFiltro.forEach((b) => b.classList.toggle("ativo", b === botao));
renderizar();
});
});
// ---------------------------------------------------------------
// renderizar(), agora com prioridade, edição e filtro
// ---------------------------------------------------------------
function criarElementoTarefa(tarefa) {
const li = document.createElement("li");
li.classList.add("tarefa");
if (tarefa.concluida) li.classList.add("concluida");
li.dataset.id = tarefa.id;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.classList.add("checkbox");
checkbox.checked = tarefa.concluida;
checkbox.addEventListener("change", () => alternarTarefa(tarefa.id));
const span = document.createElement("span");
span.classList.add("texto");
span.textContent = tarefa.texto;
span.title = "Duplo clique para editar";
habilitarEdicao(span, tarefa);
const btnRemover = document.createElement("button");
btnRemover.classList.add("btn-remover");
btnRemover.textContent = "×";
btnRemover.addEventListener("click", () => removerTarefa(tarefa.id));
li.append(criarBolinha(tarefa.prioridade), checkbox, span, btnRemover);
return li;
}
function renderizar() {
lista.innerHTML = "";
const visiveis = tarefas
.filter(FILTROS[filtroAtual])
// Alta primeiro; empate mantém a ordem de criação.
.sort((a, b) =>
PRIORIDADES[b.prioridade].peso - PRIORIDADES[a.prioridade].peso
);
if (visiveis.length === 0) {
const vazio = document.createElement("li");
vazio.classList.add("vazio");
vazio.textContent =
filtroAtual === "todas"
? "Nenhuma tarefa ainda. Adicione uma acima!"
: `Nenhuma tarefa ${filtroAtual === "pendentes" ? "pendente" : "concluída"}.`;
lista.appendChild(vazio);
atualizarContador();
return;
}
const fragmento = document.createDocumentFragment();
visiveis.forEach((tarefa) => fragmento.appendChild(criarElementoTarefa(tarefa)));
lista.appendChild(fragmento);
atualizarContador();
}
// ---------------------------------------------------------------
// O detalhe que morde: filtrar e ordenar a LISTA, não o estado
// ---------------------------------------------------------------
// `visiveis` é um array novo: filter e sort não tocam em `tarefas`.
// Se o sort rodasse direto sobre o estado, a ordem de criação estaria
// perdida para sempre depois do primeiro clique num filtro — e o
// "empate mantém a ordem de criação" deixaria de valer.
//
// O outro caminho, mais barato ainda, é nem recriar os elementos:
// esconder por CSS (`li.hidden { display: none }`) preserva o foco e o
// estado de edição, que o innerHTML = "" destrói.
innerHTML = "" a cada render é simples e destrói tudo o que o usuário estava fazendo: foco, seleção, texto sendo editado. Numa lista com edição inline isso aparece como "o campo fecha sozinho enquanto digito". Recriar tudo é aceitável enquanto a lista for pequena e nada estiver em edição — passou disso, esconda com classe em vez de recriar.
Montar elemento por elemento com createElement é mais verboso do que despejar uma string em innerHTML, e é justamente por isso que compensa: o valor entra por textContent, e o que veio de fora não tem como virar marcação. A To-Do List mostra a outra metade do problema — passado certo tamanho, o trabalho deixa de ser criar o elemento e vira manter o estado e a tela contando a mesma história.
Fontes e Referências
- MDN Web Docs — Document.createElement: https://developer.mozilla.org/pt-BR/docs/Web/API/Document/createElement
- MDN Web Docs — Node.appendChild: https://developer.mozilla.org/pt-BR/docs/Web/API/Node/appendChild
- MDN Web Docs — Element.insertAdjacentHTML: https://developer.mozilla.org/pt-BR/docs/Web/API/Element/insertAdjacentHTML
- MDN Web Docs — DocumentFragment: https://developer.mozilla.org/pt-BR/docs/Web/API/DocumentFragment
- MDN Web Docs — ChildNode.remove: https://developer.mozilla.org/en-US/docs/Web/API/Element/remove
- JavaScript.info — Modifying the Document: https://javascript.info/modifying-document
- Eloquent JavaScript, Cap. 14 — The Document Object Model: https://eloquentjavascript.net/14_dom.html
- JavaScript & JQuery — Jon Duckett (Alta Books)
Exercícios
Exercício 1
Quantos <li> a lista tem no fim, e onde eles estão? Repare que novoItem é sempre a mesma variável.
const lista = document.querySelector("#lista"); // já tem 3 <li>
const novoItem = document.createElement("li");
novoItem.textContent = "Novo item";
lista.append(novoItem);
lista.prepend(novoItem);
lista.querySelector("li:nth-child(3)").after(novoItem);
Ver resposta
✓ Resposta: A lista termina com quatro <li>, e o novo está numa posição só — depois do terceiro item. Um nó do DOM existe em um único lugar da árvore: inserir um nó que já está inserido não copia, move. O append colocou no fim, o prepend tirou do fim e levou para o começo, e o after tirou do começo e levou para depois do terceiro. É por isso que a lista de métodos de inserção do artigo, lida de cima para baixo como se fosse um roteiro, engana: cada linha desfaz a anterior. Para de fato ter quatro cópias seriam necessários quatro createElement, ou novoItem.cloneNode(true) a cada inserção. E o mesmo comportamento tem um uso legítimo: reordenar uma lista é só dar append de novo nos elementos que já estão lá, na ordem desejada.
Exercício 2
Dado o <div id="caixa"> abaixo, onde cada uma das quatro strings aparece no HTML final?
<section>
<div id="caixa"><p>original</p></div>
</section>
const caixa = document.querySelector("#caixa");
caixa.insertAdjacentHTML("beforebegin", "<b>A</b>");
caixa.insertAdjacentHTML("afterbegin", "<b>B</b>");
caixa.insertAdjacentHTML("beforeend", "<b>C</b>");
caixa.insertAdjacentHTML("afterend", "<b>D</b>");
Ver resposta
✓ Resposta: O resultado é <section><b>A</b><div id="caixa"><b>B</b><p>original</p><b>C</b></div><b>D</b></section>. Os nomes se decoram por uma regra simples: begin e end se referem ao interior do elemento, enquanto before e after dizem de que lado da fronteira você está. Assim, beforebegin e afterend caem fora, como irmãos; afterbegin e beforeend caem dentro, como primeiro e último filho. Duas consequências práticas: beforebegin e afterend não fazem nada se o elemento não tiver pai — o caso de quem acabou de criar o nó com createElement e ainda não o inseriu —, e beforeend é o único que acrescenta sem destruir, enquanto innerHTML += reconstrói todo o conteúdo e derruba os listeners que já estavam ali.
Exercício 3
Depois da última linha, quantos filhos tem o fragmento — e quantas vezes o navegador recalculou o layout?
const fragmento = document.createDocumentFragment();
for (let i = 1; i <= 100; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
fragmento.appendChild(item);
}
lista.appendChild(fragmento);
console.log(fragmento.childNodes.length);
Ver resposta
✓ Resposta: Imprime 0, e houve uma inserção no documento. O DocumentFragment é um contêiner que existe fora da árvore da página: mexer nele não dispara cálculo de layout nenhum, porque não há layout a calcular. Na hora do appendChild, o fragmento não entra no documento — ele se dissolve, e o que entra são os cem filhos, de uma vez. Por isso ele fica vazio depois, e por isso pode ser reutilizado no próximo laço sem limpeza. O contraste é o laço que dá lista.appendChild(item) a cada volta: cem inserções, cada uma capaz de invalidar o layout. Vale a ressalva honesta de que os navegadores modernos agrupam bastante trabalho sozinhos, e com cem itens a diferença é imperceptível — o fragmento importa quando a quantidade cresce, e sobretudo quando algo no meio do laço lê uma medida como offsetHeight, que força o recálculo na hora e transforma o laço em cem recálculos de verdade.
Exercício 4
O botão clonado é inserido na página. Ele reage ao clique?
const original = document.querySelector("#btn");
original.classList.add("primario");
original.dataset.acao = "salvar";
original.addEventListener("click", () => console.log("cliquei"));
const copia = original.cloneNode(true);
document.body.appendChild(copia);
console.log(copia.className); // A
console.log(copia.dataset.acao); // B
// clique na cópia → C
Ver resposta
✓ Resposta: A traz as classes, incluindo "primario", e B traz "salvar" — mas C não imprime nada: a cópia é muda. O cloneNode copia o elemento como marcação: tag, atributos, e com true também toda a descendência. Listeners registrados por addEventListener não são atributos — vivem numa lista interna presa àquele nó — e por isso não acompanham a cópia. O detalhe que confunde é que um onclick="..." escrito no HTML é atributo e sobrevive ao clone; ou seja, o mesmo comportamento se perde ou não dependendo de como foi registrado. Duas saídas: registrar o listener de novo na cópia, ou — bem melhor quando há muitas cópias — usar delegação, um listener no contêiner, que funciona para clones e para qualquer elemento criado depois.
Exercício 5
A tarefa tem id numérico. Depois destas linhas, o que sai em A, B e C — e por que o botão de remover da To-Do funciona mesmo assim?
const tarefa = { id: 3, texto: "Estudar" };
const li = document.createElement("li");
li.dataset.id = tarefa.id;
console.log(li.outerHTML); // A
console.log(li.dataset.id === tarefa.id); // B
console.log(li.dataset.id === "3"); // C
Ver resposta
✓ Resposta: A é <li data-id="3"></li>, B é false e C é true. Atributo de HTML só armazena texto: o número 3 foi convertido na atribuição, e o dataset devolve sempre string. A comparação estrita com o número original falha em silêncio — nada de erro, nada de aviso, apenas um false —, e é assim que nasce o clássico "cliquei em remover e não aconteceu nada". A To-Do do artigo escapa disso por sorte de sintaxe: em lista.querySelector(`[data-id="${id}"]`) o template literal converte o número em texto antes da busca, e os dois lados acabam sendo string. Onde a comparação é feita em JavaScript, converta de forma explícita — Number(li.dataset.id) === tarefa.id — e mantenha a conversão num lugar só, de preferência assim que o valor sai do DOM.