Criando e Removendo Elementos Dinamicamente

[68] Criando e Removendo Elementos Dinamicamente

Boa parte de uma interface não está no HTML quando a página carrega — quem a constrói é o JavaScript. Este artigo vai do createElement aos métodos modernos de inserção, passa pelo DocumentFragment que monta cem itens na memória antes de encostar no documento, e fecha numa To-Do List inteira feita à mão.
Javascript

19 min de leitura

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:

  1. Prioridade — adicione um seletor <select> com opções baixa, média e alta. Exiba uma bolinha colorida ao lado de cada tarefa
  2. Edição — ao dar duplo clique no texto, transforme-o em um <input> editável. Ao pressionar Enter ou perder o foco, salve a edição
  3. 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

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

Comentários

Mais em Javascript

Arquitetura de Software: SOLID, Clean Architecture e DDD
Arquitetura de Software: SOLID, Clean Architecture e DDD

Se houver uma frase para levar daqui, é a regra das dependências: elas apontam…

WebSockets e Comunicação em Tempo Real
WebSockets e Comunicação em Tempo Real

HTTP é uma conversa em que só o cliente pode falar primeiro. O WebSocket abre…

NPM: gerenciando pacotes e dependências
NPM: gerenciando pacotes e dependências

Todo projeto Node começa com um package.json de quinze linhas e termina com um…