Dia 12 — Eventos e construir elementos

Informatica · Conteudo · publicado em 02/10/2026
Dia 12 de 13

Eventos e construir elementos

Aula 1

addEventListener e o clique

addEventListener: o elemento avisa o que aconteceu

Até agora o JavaScript só mudou a página quando alguém chamou uma função. Com addEventListener a direção inverte: o JavaScript escuta um evento, e a função roda sozinha quando ele acontece. É o que separa um script decorativo de um programa que responde ao usuário.

Como se usa

O primeiro argumento de addEventListener é o type de evento, o nome do evento em string, e o segundo é a função ouvinte, que roda quando esse evento acontece.

const botao = document.querySelector('#salvar');
botao.addEventListener('click', () => {
  console.log('salvou');
});

A função pode ser function () {}, arrow function () => {} ou uma referência a uma função declarada com nome. addEventListener devolve undefined: não existe um objeto "botão" esperando resposta.

A forma de disparar o evento no navegador é o clique de verdade; fora dele, o elemento tem o método click(), e o evento sintético que o JavaScript cria sai de new Event('click').

Os eventos que resolvem o caso comum

EventoAssinaturaDispara quando
clickel.addEventListener('click', fn)o elemento é clicado
inputel.addEventListener('input', fn)o valor muda, a cada tecla
changeel.addEventListener('change', fn)o valor muda e o campo perde o foco
submitform.addEventListener('submit', fn)o formulário é enviado
keydownel.addEventListener('keydown', fn)a tecla é pressionada
keyupel.addEventListener('keyup', fn)a tecla é solta
focusel.addEventListener('focus', fn)o campo recebe o foco
blurel.addEventListener('blur', fn)o campo perde o foco

input e change só existem em campos de formulário. focus e blur não sobem: o evento fica no próprio elemento, enquanto click e keydown sobem até o body.

O objeto de evento

A função ouvinte recebe um objeto Event. Os dois campos mais usados são target e currentTarget, e a diferença entre eles é o que faz a delegação funcionar.

PropriedadeAssinaturaO que devolve
e.typeevento.typeo tipo de evento, em string: 'click'
e.targetevento.targeto elemento onde o clique aconteceu
e.currentTargetevento.currentTargeto elemento com o listener
e.keyevento.keya tecla, em KeyboardEvent
e.preventDefault()evento.preventDefault()cancela a ação padrão do evento
e.defaultPreventedevento.defaultPreventedtrue se algo já cancelou

O nome do parâmetro é livre — e, evento, ev são o mesmo objeto. O que importa é que ele chega como primeiro argumento da função, e que event.currentTarget só existe e só é confiável dentro do ouvinte: fora da chamada, já foi zerado.

É o event.target que resolve o caso do listener posto no pai: se o clique veio num <span> dentro do <li>, o event.target é o <span>, e é por isso que a delegação precisa do closest para chegar no elemento que interessa.

preventDefault e stopPropagation

Dois métodos do objeto de evento resolvem problemas que aparecem toda vez que um evento sobe na árvore ou dispara a ação padrão do navegador.

preventDefault: cancelar o padrão

preventDefault cancela o que o navegador faria sozinho com aquele evento. Num submit sem ele, a página recarrega e todo o JavaScript morre; num clique num <a href="#">, a URL ganha uma #.

form.addEventListener('submit', (e) => {
  e.preventDefault();
  // agora a página não recarrega e o código abaixo roda
});

O evento continua existindo e continua subindo. O que para é só a ação padrão do navegador.

stopPropagation: parar a subida

stopPropagation impede que o evento chegue aos ouvintes dos elementos acima. A forma { capture: true } no addEventListener faz a escuta acontecer na captura (*capturing*), quando o evento desce a árvore, em vez de na subida.

pai.addEventListener('click', fn, { capture: true });

Os dois juntos

MétodoAssinaturaO que segura
preventDefaultevento.preventDefault()a ação padrão do navegador
stopPropagationevento.stopPropagation()a subida para os ancestrais
stopImmediatePropagationevento.stopImmediatePropagation()a subida e os outros ouvintes do mesmo elemento

Delegação: um listener para muitos elementos

Ouvinte posto direto no elemento é atrito: uma lista de vinte itens exige vinte linhas de addEventListener, e o próximo item que entra na lista nasce sem listener. Delegação de evento resolve os dois: um listener no pai, e ele pergunta quem foi clicado.

Como se usa

lista.addEventListener('click', (e) => {
  const item = e.target.closest('li');
  if (item) item.remove();
});

O fluxo é sempre o mesmo: o clique nasce no elemento mais interno e sobe pela árvore na fase de bolha (*bubbling*), ou propagação, e é entregue a cada ancestral cujo listener está registrado sem { capture: true }. O listener do pai recebe o mesmo evento, e e.target aponta para o <li>, não para o <ul>. closest('li') sobe do ponto do clique até achar quem casa com o seletor — e devolve o próprio elemento quando ele já casa.

O mesmo vale para keydown, input e change. Delegação só funciona com evento que sobe: focus e blur não sobem, então precisam de listener direto em cada campo.

addEventListener e removeEventListener

Para remover listener, removeEventListener precisa receber a mesma referência de função que foi registrada. Passar uma arrow function nova não remove nada, porque o navegador compara a identidade da função e cada arrow function é um objeto diferente.

function avisar() { console.log('oi'); }
el.addEventListener('click', avisar);
el.removeEventListener('click', avisar);   // funciona: mesma referência

Quando o listener foi registrado em captura, o removeEventListener precisa receber { capture: true } também. Sem a opção, ele procura a assinatura na fase de bolha e não encontra.

{ once: true } faz o ouvinte rodar uma vez e se remover sozinho. É a forma curta de um contador de clique único, sem guardar a referência da função.

Um <button> dentro de um <form> é type="submit" por padrão. O click no botão é o submit do formulário, então o mesmo clique chega nos dois ouvintes. Se os dois tratam, a ação acontece duas vezes.

Exemplo

'use strict';
// ============================================================
// AULA 1 - addEventListener e o clique
// ============================================================
// O exemplo registra os ouvintes e DEPOIS dispara os eventos de verdade.
// Registrar sem disparar nao prova nada: o efeito so aparece quando o
// evento acontece, e e por isso que o exemplo dispara cada um deles.

document.body.innerHTML = `
  <h1 id="contador">0</h1>
  <button id="botao">Somar</button>
  <button id="uma-vez">Somar uma vez</button>
  <a id="link" href="#ancora">ir para a ancora</a>
  <div id="caixa">
    <ul id="tarefas">
      <li><button class="remover">Arroz</button></li>
      <li><button class="remover">Feijao</button></li>
    </ul>
  </div>
`;

// --- O contador: o padrao de todo botao ---
let total = 0;
const tela = document.querySelector('#contador');
const botao = document.querySelector('#botao');

botao.addEventListener('click', (e) => {
  total += 1;
  tela.textContent = total;
  // target e onde o clique nasceu; currentTarget e quem tem o listener.
  console.log(`  [click] total agora ${total} | target=${e.target.id} currentTarget=${e.currentTarget.id}`);
});

// addEventListener devolve undefined: nao existe "botao" esperando resposta.
console.log('addEventListener devolve:', botao.addEventListener('click', () => {}));
console.log('tela antes dos cliques:', tela.textContent);

// Agora o evento acontece de verdade. No navegador e o clique da pessoa;
// aqui e o metodo click() do elemento, que dispara o mesmo ouvinte.
botao.click();
botao.click();
console.log('tela depois de dois cliques:', tela.textContent);

// dispatchEvent monta o evento na mao, com controle do tipo e do bubbles.
botao.dispatchEvent(new Event('click', { bubbles: true }));
console.log('tela depois do dispatchEvent:', tela.textContent);

// --- once: um listener que se remove sozinho ---
const umaVez = document.querySelector('#uma-vez');
umaVez.addEventListener('click', (e) => {
  console.log('  [once] rodou para', e.currentTarget.id);
}, { once: true });
umaVez.click();
umaVez.click();
umaVez.click();
console.log('tres cliques, uma linha de log: o once gastou o listener');

// --- preventDefault: cancelar a acao padrao do navegador ---
const link = document.querySelector('#link');
link.addEventListener('click', (e) => {
  e.preventDefault();
  console.log('  [link] preventDefault rodou | defaultPrevented:', e.defaultPrevented);
});
link.dispatchEvent(new Event('click', { bubbles: true, cancelable: true }));

// --- bubbling: o evento nasce no button e sobe ate o <div> ---
console.log('\n=== bubbling: o evento sobe da folha para a raiz ===');
const caixa = document.querySelector('#caixa');
const lista = document.querySelector('#tarefas');

caixa.addEventListener('click', (e) => {
  console.log('  [3º nivel] <div id="caixa"> ouviu o click de', e.target.textContent);
});
lista.addEventListener('click', (e) => {
  console.log('  [2º nivel] <ul> ouviu o click de', e.target.textContent);
  e.stopPropagation();   // corta aqui: a <div> de cima nao vai saber
});
lista.firstElementChild.addEventListener('click', (e) => {
  console.log('  [1º nivel] <li> ouviu o click de', e.target.textContent);
});
lista.querySelector('button').dispatchEvent(new Event('click', { bubbles: true }));
console.log('o stopPropagation no <ul> segurou a subida antes da <div>');

// --- capture: o mesmo evento, ouvido na descida em vez da subida ---
console.log('\n=== capture: o mesmo evento, ouvido antes ===');
// Para tirar um listener em captura depois, o removeEventListener precisa
// receber a mesma opcao -- e a mesma referencia de funcao.
function capturar(e) {
  console.log('  [captura] <div> ouviu ANTES do <li>, target:', e.target.tagName);
}
caixa.addEventListener('click', capturar, { capture: true });
lista.querySelector('button').dispatchEvent(new Event('click', { bubbles: true }));

caixa.removeEventListener('click', capturar, { capture: true });
console.log('  [captura] tirei o listener da <div>; o proximo disparo nao imprime a captura');
caixa.querySelector('button').dispatchEvent(new Event('click', { bubbles: true }));

// --- Delegacao: um listener so, no pai, para todos os filhos ---
console.log('\n=== delegacao: um listener para os dois itens ===');
const tarefas = document.querySelector('#tarefas');
tarefas.addEventListener('click', (e) => {
  // target e o <button>; closest sobe ate o <li>, que e quem interessa.
  const botaoDoItem = e.target.closest('.remover');
  if (!botaoDoItem) return;   // o clique nao veio de um botao de remover
  console.log('  [delegado] removo', botaoDoItem.textContent);
  botaoDoItem.closest('li').remove();
});
console.log('itens antes:', tarefas.children.length);
tarefas.querySelectorAll('.remover').forEach((b) => {
  b.dispatchEvent(new Event('click', { bubbles: true }));
});
console.log('itens depois:', tarefas.children.length);

// --- removeEventListener precisa da MESMA referencia da funcao ---
console.log('\n=== removeEventListener ===');
function mostrar(evento) {
  console.log('  [ouvinte] ainda ativo em', evento.currentTarget.id);
}
botao.addEventListener('click', mostrar);
botao.dispatchEvent(new Event('click', { bubbles: true }));
botao.removeEventListener('click', mostrar);
botao.dispatchEvent(new Event('click', { bubbles: true }));
console.log('o segundo disparo nao imprimiu [ouvinte]: o listener saiu');

Saída real

addEventListener devolve: undefined
tela antes dos cliques: 0
  [click] total agora 1 | target=botao currentTarget=botao
  [click] total agora 2 | target=botao currentTarget=botao
tela depois de dois cliques: 2
  [click] total agora 3 | target=botao currentTarget=botao
tela depois do dispatchEvent: 3
  [once] rodou para uma-vez
tres cliques, uma linha de log: o once gastou o listener
  [link] preventDefault rodou | defaultPrevented: true

=== bubbling: o evento sobe da folha para a raiz ===
  [1º nivel] <li> ouviu o click de Arroz
  [2º nivel] <ul> ouviu o click de Arroz
o stopPropagation no <ul> segurou a subida antes da <div>

=== capture: o mesmo evento, ouvido antes ===
  [captura] <div> ouviu ANTES do <li>, target: BUTTON
  [1º nivel] <li> ouviu o click de Arroz
  [2º nivel] <ul> ouviu o click de Arroz
  [captura] tirei o listener da <div>; o proximo disparo nao imprime a captura
  [1º nivel] <li> ouviu o click de Arroz
  [2º nivel] <ul> ouviu o click de Arroz

=== delegacao: um listener para os dois itens ===
itens antes: 2
  [1º nivel] <li> ouviu o click de Arroz
  [2º nivel] <ul> ouviu o click de Arroz
  [delegado] removo Arroz
  [2º nivel] <ul> ouviu o click de Feijao
  [delegado] removo Feijao
itens depois: 0

=== removeEventListener ===
  [click] total agora 4 | target=botao currentTarget=botao
  [ouvinte] ainda ativo em botao
  [click] total agora 5 | target=botao currentTarget=botao
o segundo disparo nao imprimiu [ouvinte]: o listener saiu
Aula 2

Criar elementos, eventos de teclado e formulario

Criar elemento: a página montada pelo JavaScript

document.createElement devolve um elemento novo, vazio e fora da página. Ele só passa a existir visualmente depois de ser inserido em algum lugar — o appendChild no body, no prepend no topo, ou o replaceChildren trocando tudo.

Como se usa

const item = document.createElement('li');
item.className = 'item';
item.textContent = 'Arroz';
document.querySelector('#lista').appendChild(item);

O elemento criado começa com parentElement igual a null. Inserir não copia: é o mesmo objeto, o mesmo que a variável segura, e a partir daí a variável e a página apontam para a mesma coisa. Nenhuma das duas etapas faz a outra: criar não adicionar à página, e inserir sem criar exige um elemento que já existe.

Criar e inserir

MétodoAssinaturaEfeito
createElementdocument.createElement('li')elemento novo, fora da página
createTextNodedocument.createTextNode('texto')nó de texto puro, nodeType 3
appendChildpai.appendChild(filho)insere no fim, devolve o filho
prependpai.prepend(filho)insere no começo
appendpai.append(filho, outro)insere no fim, aceita vários e texto
insertBeforepai.insertBefore(novo, referencia)insere antes do irmão indicado
removeChildpai.removeChild(filho)remove o filho pelo pai, devolve o filho
replaceChildrenpai.replaceChildren(...novos)esvazia e insere tudo de uma vez
removefilho.remove()tira o elemento da página
replaceWithvelho.replaceWith(novo)troca um elemento por outro

Adicionar, remover e trocar

append aceita texto direto (pai.append('oi')) e vários argumentos de uma vez. appendChild aceita um só e exige um nó, o que é mais verboso e mais fácil de errar.

remove() age no próprio elemento e não precisa saber o pai: é o que remover elemento da página pede. removeChild é o caminho inverso: o pai é quem remove, e o argumento tem de ser filho dele — um elemento de outra árvore devolve NotFoundError.

Para tirar da página, o remove() resolve. Para renderizar de novo depois, é preciso ter o elemento guardado: um li removido e esquecido não volta sozinho, e só a referência guardada em uma variável ou array consegue recriá-lo.

A ordem dos filhos e o render

appendChild é nome de método, mas o comportamento é o contrário do que o nome sugere: ele coloca sempre no fim. Não existe posição numérica no método — para o meio da lista está insertBefore, que precisa do irmão de referência.

Onde o novo elemento entraMétodo
no fimappendChild ou append
no começoprepend
antes de um irmãoinsertBefore(novo, irmao)

insertBefore com referência null faz o mesmo que appendChild. DocumentFragment resolve o caso das inserções repetidas: monta-se os nós num fragmento solto e insere-se o fragmento inteiro de uma vez, com uma única atualização da tela.

O uso que mais aparece de tudo isso é o render de uma lista dinâmica: um array de dados, um container na tela, e uma função que transforma cada item em elemento. É a mesma ideia de gerar lista que toda página dinâmica faz.

const lista = document.querySelector('#lista');
function renderizar(itens) {
  lista.replaceChildren();
  for (const item of itens) {
    const li = document.createElement('li');
    li.classList.add('item');
    li.textContent = item;
    lista.appendChild(li);
  }
}

Como renderizar esvazia o container antes, chamá-la de novo com outro array substitui a lista inteira, sem duplicar item e sem precisar apagar um por um.

Eventos de teclado

Teclado exige teclado de verdade. new Event('keydown') cria um evento sem nenhuma tecla, e evento.key vem undefined. O objeto certo é o KeyboardEvent, que aceita a tecla no construtor.

Como se usa

const campo = document.querySelector('#busca');
campo.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault();
    buscar(campo.value);
  }
});

O que o KeyboardEvent traz

PropriedadeAssinaturaO que devolve
e.keyevento.keya tecla: 'Enter', 'a', 'Escape', 'ArrowUp'
e.codeevento.codea tecla no teclado: 'KeyA', 'Digit1', 'Enter'
e.repeatevento.repeattrue se a tecla está sendo segurada
e.ctrlKeyevento.ctrlKeytrue com Ctrl apertado
e.altKey, e.shiftKey, e.metaKeyevento.altKey e afinso mesmo para as outras modificadoras

key e code respondem perguntas diferentes. key é o que o usuário vê no caracter e muda com o teclado dele; code é a posição física, e é ele que serve para atalho e para layout em que a posição importa.

keydown dispara antes de o caracter entrar no campo, keyup depois. No keydown com e.repeat verdadeiro, o evento chega várias vezes seguidas — é assim que vem a repetição quando a tecla é segurada.

Formulário: submit, input e validação

Formulário é o lugar onde o DOM vira entrada de dados. O <form> não precisa de action nem de botão para o JavaScript funcionar: basta escutar o submit e impedir a recarga da página.

Como se usa

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const dados = new FormData(form);
  console.log(dados.get('nome'));
});
ObjetoAssinaturaO que faz
form.addEventListenerform.addEventListener('submit', fn)roda na tentativa de envio
new FormData(form)new FormData(elementoForm)pares nome/valor de todos os campos
fd.get(nome)formData.get('nome')o valor de um campo, ou null
fd.entries()formData.entries()iterável de [nome, valor]
form.checkValidity()form.checkValidity()false se algum campo obrigatório falta
campo.valuecampo.valueo texto digitado no campo

FormData pega só os campos que têm atributo name. Um input sem name é invisível para ele, mesmo aparecendo na tela.

O fluxo dos três eventos do campo

input dispara a cada tecla e é o que se usa para validar enquanto digita. change espera o campo perder o foco, então serve para o caso em que a validação a cada tecla atrapalha. focus e blur dizem quando o campo entrou e saiu.

campo.addEventListener('input', (e) => {
  const vazio = e.target.value.trim() === '';
  e.target.classList.toggle('invalido', vazio);
});

Usar e.target e não o nome da variável vale para campo criado em laço: o e.target é sempre o elemento exato que disparou o evento.

Validação de formulário em JavaScript

A validação de formulário escrita em JavaScript é o que transforma o input em regra de negócio: a mensagem de erro vai para a tela, o campo ganha uma classe, e o envio só continua se a regra passar.

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const email = document.querySelector('#email').value;
  if (!email.includes('@')) {
    e.target.querySelector('#erro').textContent = 'E-mail inválido';
    return;   // impede o envio
  }
  console.log('envia o formulario');
});

O return antecipado é o que impede a ação de continuar com o dado inválido: sem ele, o código abaixo do if roda mesmo quando o e-mail está errado.

A validação do navegador (required, type="email", minlength) é feita antes do submit, e o navegador bloqueia o envio se algum campo estiver inválido. Para interceptar antes, use form.addEventListener('submit', fn, { capture: true }) — na captura o listener roda antes da validação padrão.

O alvo do evento e a delegação

O target é o elemento mais interno onde o evento nasceu; o currentTarget é o elemento que está ouvindo. Com dois níveis de ancestrais, essa diferença é o que permite ao pai decidir o que fazer sem saber de antemão qual filho foi tocado.

Delegação com teclado e com clique

container.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.target.closest('li').remove();
  }
});

A fase de bolha é o que sustenta a delegação de evento: sem propagação do filho para o pai, o listener do pai nunca veria o evento.

Precisa de delegação?Motivo
click, keydown, keyupsobem pela árvore, um listener no pai alcança todos os filhos
input, changesobem, e o e.target é sempre o campo que mudou
focus, blurnão sobem; exigem listener direto em cada campo
submitsobe a partir do formulário, então um listener no form basta

A fase de bolha é uma das três que todo evento percorre, e elas têm nome: capturing (o evento desce do window até o elemento, do mais externo para o mais interno), bubbling (sobe do elemento até a window, do mais interno para o mais externo), e o target, o elemento onde ele nasceu. Por padrão o listener roda na fase de bolha; para rodar na de captura, é o terceiro argumento do addEventListener:

pai.addEventListener('click', fn, { capture: true });   // roda na descida
pai.addEventListener('click', fn);                      // padrão: sobe

O { capture: true } é o que permite a delegação de formulário: ele roda na descida, antes da validação padrão do navegador, e é por isso que intercepta o que a validação nativa bloquearia depois.

A delegação economiza linhas e sobrevive à criação de elementos novos: um li acrescentado depois já nasce com o comportamento, porque o listener está no pai e não nele.

event.target é o ponto exato do evento, que nem sempre é o elemento com o listener — pode ser o <span> dentro do <li>, e por isso o closest('li'). E e.target pode ser o document num evento disparado em nível de página, onde .closest não existe: nesse caso o closest direto quebra com TypeError.

Exemplo

'use strict';
// ============================================================
// AULA 2 - Criar elementos, eventos de teclado e formulario
// ============================================================
// O exemplo monta uma lista dinamica com createElement, escuta o teclado
// num campo de busca e trata o envio do formulario. Cada evento e
// disparado depois de registrado, para o efeito aparecer na saida.

document.body.innerHTML = `
  <form id="cadastro">
    <input id="nome" name="nome" placeholder="nome" required>
    <input id="email" name="email" type="email" placeholder="e-mail" required>
    <button type="submit">Cadastrar</button>
  </form>
  <input id="busca" placeholder="buscar na lista">
  <p id="status"></p>
  <ul id="lista"></ul>
`;

const lista = document.querySelector('#lista');
const status = document.querySelector('#status');

// --- createElement: nasce fora da pagina, entra com appendChild ---
console.log('=== createElement ===');
const primeiro = document.createElement('li');
primeiro.className = 'item';
primeiro.textContent = 'Arroz';
console.log('antes de inserir, parentElement:', primeiro.parentElement);
console.log('tagName do elemento novo:', primeiro.tagName, '| nodeType:', primeiro.nodeType);

lista.appendChild(primeiro);
console.log('depois do appendChild, parentElement:', primeiro.parentElement.tagName);
console.log('appendChild devolve o proprio filho:', lista.appendChild(primeiro) === primeiro);
console.log('o <ul> tem', lista.children.length, 'item(s) e o HTML e:', lista.innerHTML);

// --- append aceita varios e texto; prepend e no começo ---
const segundo = document.createElement('li');
segundo.textContent = 'Cafe';
const terceiro = document.createElement('li');
terceiro.textContent = 'Leite';
lista.append(segundo, terceiro);
console.log('apos append de dois:', lista.children.length, '| ultimo:', lista.lastElementChild.textContent);

const primeiroDaLista = document.createElement('li');
primeiroDaLista.textContent = 'Acucar';
lista.prepend(primeiroDaLista);
console.log('apos prepend, o primeiro e:', lista.firstElementChild.textContent);

// insertBefore precisa do irmao de referencia: nao ha posicao numerica.
const antesDoCafe = document.createElement('li');
antesDoCafe.textContent = 'Acar';
lista.insertBefore(antesDoCafe, lista.querySelector('li:nth-child(3)'));
console.log('apos insertBefore:', [...lista.children].map((li) => li.textContent).join(', '));

// createTextNode monta um no de texto puro, sem tag.
const texto = document.createTextNode(' (novo) ');
const marca = document.createElement('strong');
marca.appendChild(texto);
lista.firstElementChild.appendChild(marca);
console.log('createTextNode tem nodeType', texto.nodeType, 'e nodeName', texto.nodeName);
console.log('primeiro item com o texto aninhado:', lista.firstElementChild.textContent);

// --- replaceChildren: esvazia e insere tudo de uma vez ---
const render = (itens) => {
  lista.replaceChildren();
  for (const item of itens) {
    const li = document.createElement('li');
    li.className = 'item';
    li.textContent = item;
    lista.appendChild(li);
  }
  return lista.children.length;
};
console.log('\n=== replaceChildren: redesenhar a lista inteira ===');
console.log('itens redesenhados:', render(['Feijao', 'Cafe', 'Leite']));
console.log('HTML final da lista:', lista.innerHTML);

// --- remove e replaceWith ---
console.log('\n=== remover e trocar ===');
const alvo = lista.querySelector('li:first-child');
console.log('vou remover:', alvo.textContent);
alvo.remove();
console.log('restaram', lista.children.length, 'itens');

// --- Teclado: e KeyboardEvent, nao Event ---
console.log('\n=== teclado ===');
const busca = document.querySelector('#busca');
busca.addEventListener('keydown', (e) => {
  console.log(`  [keydown] key=${e.key} code=${e.code} ctrl=${e.ctrlKey} repeat=${e.repeat}`);
  if (e.key === 'Enter') {
    e.preventDefault();
    const termo = busca.value.trim().toLowerCase();
    const achados = [...lista.children].filter((li) => li.textContent.toLowerCase().includes(termo));
    status.textContent = `${achados.length} item(ns) para "${termo}"`;
    console.log('  [Enter] status na tela:', status.textContent);
  }
  if (e.key === 'Escape') {
    busca.value = '';
    status.textContent = '';
    console.log('  [Escape] campo limpo');
  }
});

busca.value = 'CA';
busca.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
busca.value = 'zzz';
busca.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
busca.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
busca.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'a', code: 'KeyA', ctrlKey: true, repeat: true, bubbles: true }));
// new Event('keydown') puro nao tem tecla nenhuma: e o que quebra o exemplo.
console.log('new Event("keydown").key vale', new Event('keydown').key, '| new KeyboardEvent tem:', new window.KeyboardEvent('keydown', { key: 'x' }).key);

// --- Formulario: input, change, focus, blur e submit ---
console.log('\n=== formulario ===');
const form = document.querySelector('#cadastro');
const nome = document.querySelector('#nome');
const email = document.querySelector('#email');

nome.addEventListener('focus', (e) => console.log('  [focus] entrou em', e.target.id));
nome.addEventListener('input', (e) => {
  e.target.classList.toggle('invalido', e.target.value.trim() === '');
  console.log('  [input] a cada tecla, value agora:', JSON.stringify(e.target.value));
});
nome.addEventListener('blur', (e) => console.log('  [blur] saiu de', e.target.id));
nome.addEventListener('change', (e) => console.log('  [change] value no fim:', JSON.stringify(e.target.value)));

nome.dispatchEvent(new Event('focus'));
nome.value = 'Ana';
nome.dispatchEvent(new Event('input', { bubbles: true }));
nome.dispatchEvent(new Event('change', { bubbles: true }));
nome.dispatchEvent(new Event('blur'));

// O e-mail tem type="email" e required: vazio, ele invalida o formulario
// inteiro, mesmo com o nome preenchido.
console.log('so o nome preenchido, checkValidity:', form.checkValidity());
console.log('tipo do campo:', email.type, '| validacao vazia:', email.validity.valueMissing);
email.value = '[email protected]';
console.log('com os dois preenchidos, checkValidity:', form.checkValidity());

// submit: sem preventDefault a pagina recarrega e o script morre.
form.addEventListener('submit', (e) => {
  e.preventDefault();
  console.log('  [submit] pagina nao recarrega | defaultPrevented:', e.defaultPrevented);
  if (!form.checkValidity()) {
    console.log('  [submit] checkValidity:', false, '-> o navegador barra o envio');
    return;
  }
  // FormData le so os campos que tem atributo name.
  const dados = Object.fromEntries(new window.FormData(form).entries());
  console.log('  [submit] FormData:', JSON.stringify(dados));
  render([...lista.children].map((li) => li.textContent));
  const novo = document.createElement('li');
  novo.className = 'item novo';
  novo.textContent = dados.nome;
  lista.appendChild(novo);
  console.log('  [submit] item acrescentado, a lista tem', lista.children.length, 'itens');
  form.reset();
});
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));

// Com o campo vazio, o required do navegador trava o envio.
console.log('\n=== required barra o envio ===');
form.reset();
console.log('apos o reset, nome:', JSON.stringify(nome.value), '| checkValidity:', form.checkValidity());
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
console.log('o nada foi acrescentado a lista, ela segue com', lista.children.length, 'itens');

// --- Delegacao de teclado: um listener no painel, para todos os <li> ---
console.log('\n=== delegacao de teclado ===');
const painel = document.createElement('div');
painel.id = 'painel';
lista.replaceWith(painel);
painel.appendChild(lista);
painel.addEventListener('keydown', (e) => {
  if (e.key !== 'Enter') return;
  const item = e.target.closest('li');
  if (!item) return;
  console.log('  [delegado] Enter no item', item.textContent, '- veio de um <li> que nao tem listener proprio');
  item.classList.add('marcado');
});
// O foco vai para o <li> e a tecla sobe: e o <li> que nao tem listener
// nenhum que repassa o Enter para o painel.
lista.firstElementChild.tabIndex = 0;
lista.firstElementChild.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
console.log('o painel recebeu o Enter vindo do <li>, sem nenhum addEventListener no item');

Saída real

=== createElement ===
antes de inserir, parentElement: null
tagName do elemento novo: LI | nodeType: 1
depois do appendChild, parentElement: UL
appendChild devolve o proprio filho: true
o <ul> tem 1 item(s) e o HTML e: <li class="item">Arroz</li>
apos append de dois: 3 | ultimo: Leite
apos prepend, o primeiro e: Acucar
apos insertBefore: Acucar, Arroz, Acar, Cafe, Leite
createTextNode tem nodeType 3 e nodeName #text
primeiro item com o texto aninhado: Acucar (novo) 

=== replaceChildren: redesenhar a lista inteira ===
itens redesenhados: 3
HTML final da lista: <li class="item">Feijao</li><li class="item">Cafe</li><li class="item">Leite</li>

=== remover e trocar ===
vou remover: Feijao
restaram 2 itens

=== teclado ===
  [keydown] key=Enter code= ctrl=false repeat=false
  [Enter] status na tela: 1 item(ns) para "ca"
  [keydown] key=Enter code= ctrl=false repeat=false
  [Enter] status na tela: 0 item(ns) para "zzz"
  [keydown] key=Escape code= ctrl=false repeat=false
  [Escape] campo limpo
  [keydown] key=a code=KeyA ctrl=true repeat=true
new Event("keydown").key vale undefined | new KeyboardEvent tem: x

=== formulario ===
  [focus] entrou em nome
  [input] a cada tecla, value agora: "Ana"
  [change] value no fim: "Ana"
  [blur] saiu de nome
so o nome preenchido, checkValidity: false
tipo do campo: email | validacao vazia: true
com os dois preenchidos, checkValidity: true
  [submit] pagina nao recarrega | defaultPrevented: true
  [submit] FormData: {"nome":"Ana","email":"[email protected]"}
  [submit] item acrescentado, a lista tem 3 itens

=== required barra o envio ===
apos o reset, nome: "" | checkValidity: false
  [submit] pagina nao recarrega | defaultPrevented: true
  [submit] checkValidity: false -> o navegador barra o envio
o nada foi acrescentado a lista, ela segue com 3 itens

=== delegacao de teclado ===
  [delegado] Enter no item Cafe - veio de um <li> que nao tem listener proprio
o painel recebeu o Enter vindo do <li>, sem nenhum addEventListener no item