Dia 12 — Eventos e construir elementos
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
| Evento | Assinatura | Dispara quando |
|---|---|---|
click | el.addEventListener('click', fn) | o elemento é clicado |
input | el.addEventListener('input', fn) | o valor muda, a cada tecla |
change | el.addEventListener('change', fn) | o valor muda e o campo perde o foco |
submit | form.addEventListener('submit', fn) | o formulário é enviado |
keydown | el.addEventListener('keydown', fn) | a tecla é pressionada |
keyup | el.addEventListener('keyup', fn) | a tecla é solta |
focus | el.addEventListener('focus', fn) | o campo recebe o foco |
blur | el.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.
| Propriedade | Assinatura | O que devolve |
|---|---|---|
e.type | evento.type | o tipo de evento, em string: 'click' |
e.target | evento.target | o elemento onde o clique aconteceu |
e.currentTarget | evento.currentTarget | o elemento com o listener |
e.key | evento.key | a tecla, em KeyboardEvent |
e.preventDefault() | evento.preventDefault() | cancela a ação padrão do evento |
e.defaultPrevented | evento.defaultPrevented | true 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étodo | Assinatura | O que segura |
|---|---|---|
preventDefault | evento.preventDefault() | a ação padrão do navegador |
stopPropagation | evento.stopPropagation() | a subida para os ancestrais |
stopImmediatePropagation | evento.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. Oclickno botão é osubmitdo 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
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étodo | Assinatura | Efeito |
|---|---|---|
createElement | document.createElement('li') | elemento novo, fora da página |
createTextNode | document.createTextNode('texto') | nó de texto puro, nodeType 3 |
appendChild | pai.appendChild(filho) | insere no fim, devolve o filho |
prepend | pai.prepend(filho) | insere no começo |
append | pai.append(filho, outro) | insere no fim, aceita vários e texto |
insertBefore | pai.insertBefore(novo, referencia) | insere antes do irmão indicado |
removeChild | pai.removeChild(filho) | remove o filho pelo pai, devolve o filho |
replaceChildren | pai.replaceChildren(...novos) | esvazia e insere tudo de uma vez |
remove | filho.remove() | tira o elemento da página |
replaceWith | velho.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 entra | Método |
|---|---|
| no fim | appendChild ou append |
| no começo | prepend |
| antes de um irmão | insertBefore(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
| Propriedade | Assinatura | O que devolve |
|---|---|---|
e.key | evento.key | a tecla: 'Enter', 'a', 'Escape', 'ArrowUp' |
e.code | evento.code | a tecla no teclado: 'KeyA', 'Digit1', 'Enter' |
e.repeat | evento.repeat | true se a tecla está sendo segurada |
e.ctrlKey | evento.ctrlKey | true com Ctrl apertado |
e.altKey, e.shiftKey, e.metaKey | evento.altKey e afins | o 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')); });
| Objeto | Assinatura | O que faz |
|---|---|---|
form.addEventListener | form.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.value | campo.value | o 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 dosubmit, e o navegador bloqueia o envio se algum campo estiver inválido. Para interceptar antes, useform.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, keyup | sobem pela árvore, um listener no pai alcança todos os filhos |
input, change | sobem, e o e.target é sempre o campo que mudou |
focus, blur | não sobem; exigem listener direto em cada campo |
submit | sobe 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 oclosest('li'). Ee.targetpode ser odocumentnum evento disparado em nível de página, onde.closestnão existe: nesse caso oclosestdireto quebra comTypeError.
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