Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Objeto event, preventDefault e delegação de eventos

O parâmetro event que o navegador entrega sozinho, a diferença entre target e currentTarget, as três fases do evento e delegação com saída real de cada uma.

Rodolfo Mori6 min de leitura

Todo listener recebe um argumento que você não passou: o objeto do evento. Ele carrega o que aconteceu (type), onde aconteceu (target), quem está ouvindo (currentTarget) e os dois métodos que mais confundem — preventDefault, que cancela a ação padrão do navegador, e stopPropagation, que impede o evento de subir para os elementos de fora.

Não são sinônimos. preventDefault diz “o link não navega”; stopPropagation diz “o <div> de fora não fica sabendo do clique”. Trocar um pelo outro é o que faz um formulário recarregar a página apesar de o seu código estar lá.

A lição anterior mostrou como registrar o listener com addEventListener; esta é sobre o que chega dentro dele.

O formulário de ocorrência que chega com cada evento

Quando toca o alarme de uma loja, o segurança não recebe apenas “algo aconteceu”. A ficha informa qual sensor disparou, em que setor ele está e por quais áreas o aviso passou. O navegador faz algo parecido com cada clique, digitação ou envio de formulário.

Essa ficha é o objeto Event. target aponta para a origem, currentTarget para quem está executando o listener, e os métodos alteram o comportamento normal ou a propagação. Na primeira execução, imprima mentalmente esses dois alvos antes de conferir a saída: se forem diferentes, você já localizou onde o evento nasceu e onde está sendo tratado.

Você declara o nome; o valor vem de fora.

js
const carrinho = document.querySelector('#carrinho');

carrinho.addEventListener('click', (evento) => {
  console.log('type:', evento.type);
  console.log('target:', evento.target.tagName, '|', evento.target.className);
  console.log('currentTarget:', evento.currentTarget.tagName, '|', evento.currentTarget.id);
  console.log('bubbles:', evento.bubbles, '| cancelable:', evento.cancelable);
});

Clicando no botão “Remover” do primeiro item da lista:

type: click target: BUTTON | remover currentTarget: UL | carrinho bubbles: true | cancelable: true

Esse evento é o mesmo objeto para todos os listeners daquele disparo. event, e, evt, evento — o nome é seu; a convenção mais comum em projeto brasileiro é evento ou e. Como qualquer parâmetro de função, ele só existe dentro do listener.

target não é currentTarget

Essa distinção é o coração da delegação.

propriedade aponta para muda durante a propagação
event.target o elemento mais interno que originou o evento não, é sempre o mesmo
event.currentTarget o elemento onde este listener está pendurado sim, um por listener

No exemplo acima, o clique nasceu no <button class="remover"> — esse é o target. O listener estava no <ul id="carrinho"> — esse é o currentTarget. O <button> nem tem listener; ele só serviu de ponto de partida.

As três fases

Um clique não acontece só no elemento clicado. Ele desce da raiz até o alvo (captura), acontece no alvo (alvo) e sobe de volta (borbulha). Cada listener escolhe em qual dessas passagens quer entrar: o quarto argumento true (ou { capture: true }) coloca o listener na descida.

js
const secao = document.querySelector('#checkout');
const form = document.querySelector('#pagamento');
const botao = document.querySelector('#confirmar');
const fase = { 1: 'captura', 2: 'alvo', 3: 'borbulha' };

secao.addEventListener('click', (e) => console.log('section:', fase[e.eventPhase]), true);
form.addEventListener('click', (e) => console.log('form:', fase[e.eventPhase]), true);
botao.addEventListener('click', (e) => console.log('button:', fase[e.eventPhase]));
form.addEventListener('click', (e) => console.log('form:', fase[e.eventPhase]));
secao.addEventListener('click', (e) => console.log('section:', fase[e.eventPhase]));

Um único clique no botão:

section: captura form: captura button: alvo form: borbulha section: borbulha

Cinco listeners, um clique. A ordem não é a ordem em que você escreveu o código — é a geometria do HTML. Por padrão (capture ausente ou false), todo listener fica na borbulha — a fase em que o evento sobe de dentro para fora, do alvo em direção à raiz. É por isso que um listener pendurado no pai recebe os cliques que nasceram nos filhos.

preventDefault cancela a ação; stopPropagation cancela a viagem

Ação padrão é o que o navegador faria sozinho: link navega, formulário envia, checkbox marca, botão direito abre o menu de contexto.

js
const card = document.querySelector('#card-produto');
const link = document.querySelector('#detalhes');

card.addEventListener('click', () => console.log('card: abriu o preview lateral'));

link.addEventListener('click', (evento) => {
  evento.preventDefault();
  console.log('link: navegação cancelada?', evento.defaultPrevented);
});
link: navegação cancelada? true card: abriu o preview lateral

A navegação foi cancelada e mesmo assim o card recebeu o clique — porque preventDefault não tem nada a ver com propagação. Trocando para stopPropagation:

js
const card = document.querySelector('#card-produto');
const link = document.querySelector('#detalhes');

card.addEventListener('click', () => console.log('card: abriu o preview lateral'));

link.addEventListener('click', (evento) => {
  evento.stopPropagation();
  console.log('link: parei a subida');
});
link: parei a subida

Agora o card não soube de nada — e o navegador navegou, porque ninguém cancelou a ação padrão. Os dois são independentes e resolvem problemas diferentes:

método cancela não cancela
preventDefault() navegar, enviar, marcar, arrastar os outros listeners
stopPropagation() listeners nos elementos de fora a ação padrão do navegador
stopImmediatePropagation() tudo acima e os outros listeners do mesmo elemento a ação padrão

return false dentro de um listener de addEventListener não faz nenhuma das duas coisas — isso é herança de onclick inline e não vale aqui.

Delegação: um listener para uma lista que muda

Sem delegação, você registra um listener por item — e os itens criados depois ficam de fora:

js
const carrinho = document.querySelector('#carrinho');

document.querySelectorAll('.remover').forEach((botao) => {
  botao.addEventListener('click', () => console.log('removendo', botao.closest('li').dataset.id));
});

carrinho.insertAdjacentHTML('beforeend',
  '<li data-id="8830">Mouse sem fio <button class="remover">Remover</button></li>');

Clicando nos dois botões “Remover”:

removendo 7712

O segundo botão não respondeu. Ele nasceu depois do forEach, e forEach não tem como voltar no tempo. Com delegação, o listener mora no <ul>, que já existe desde o começo:

js
const carrinho = document.querySelector('#carrinho');

carrinho.addEventListener('click', (evento) => {
  const botao = evento.target.closest('.remover');
  if (!botao) return;

  const item = botao.closest('li');
  console.log('removendo item', item.dataset.id);
  item.remove();
});

carrinho.insertAdjacentHTML('beforeend',
  '<li data-id="8830">Mouse sem fio <button class="remover">Remover</button></li>');

Clicando nos dois botões:

removendo item 7712 removendo item 8830

Duas linhas fazem o trabalho. evento.target.closest('.remover') sobe a partir de onde o clique nasceu procurando o botão — resolve o caso do usuário clicar num <svg> ou num <span> dentro do botão, que é onde o evento.target === botao falha. E if (!botao) return descarta os cliques em qualquer outro ponto da lista.

Erros comuns

O formulário recarrega a página

O sintoma é conhecido: você clica em “Aplicar”, o console pisca e some, a página volta ao estado inicial. Não há erro nenhum — o navegador só fez o que sempre fez.

js
document.querySelector('#aplicar').addEventListener('click', () => {
  console.log('validando o cupom FRETE10...');
});
URL antes : https://loja.club/checkout validando o cupom FRETE10... URL depois: https://loja.club/aplicar-cupom?codigo=FRETE10

O listener rodou, e logo em seguida o navegador enviou o formulário: um <button> dentro de <form> é type="submit" por padrão. A correção é escutar o submit do formulário — não o click do botão — e cancelar a ação padrão:

js
const form = document.querySelector('#cupom');

form.addEventListener('submit', (evento) => {
  evento.preventDefault();
  console.log('validando o cupom FRETE10...');
  console.log('navegação cancelada:', evento.defaultPrevented);
});
validando o cupom FRETE10... navegação cancelada: true URL continua: https://loja.club/checkout

Chamar o handler sem passar o evento

Quando você embrulha o handler numa arrow que não repassa o argumento, o parâmetro chega undefined:

js
function fecharPedido(evento) {
  evento.preventDefault();
  console.log('pedido fechado');
}

const form = document.querySelector('#pedido');
form.addEventListener('submit', () => fecharPedido());

Isolando a chamada — que é exatamente o que a arrow faz:

file:///private/tmp/loja/checkout-pedido.mjs:5 evento.preventDefault(); ^

TypeError: Cannot read properties of undefined (reading ‘preventDefault’) at fecharPedido (file:///private/tmp/loja/checkout-pedido.mjs:5:10) at file:///private/tmp/loja/checkout-pedido.mjs:11:1 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

Passe a função direto — form.addEventListener('submit', fecharPedido) — ou repasse o argumento: (evento) => fecharPedido(evento). O mesmo vale para onsubmit="fecharPedido()" escrito no HTML, que também chama sem argumento.

stopPropagation usado como martelo

Colocar stopPropagation em todo listener “para não dar conflito” é o bug mais caro deste texto, porque ele só aparece semanas depois: o menu que fecha ao clicar fora para de fechar, o analytics deixa de registrar cliques numa área da página, o atalho global some. Antes de parar o evento, pergunte se um if (evento.target.closest('#menu')) return no listener de cima não resolve.

O que levar para o próximo formulário

  • Escute o evento certo: submit no <form>, não click no botão.
  • preventDefault na primeira linha do handler de submit, sempre.
  • Um listener no container com closest, não um por item.
  • evento.target para saber onde o usuário clicou; evento.currentTarget para saber onde você está ouvindo.
  • stopPropagation só quando você conseguir explicar qual listener de fora está atrapalhando.

A próxima lição usa tudo isso para pegar o valor de um input — e mostra por que value continua sendo string mesmo num campo type="number". O guia completo de JavaScript tem a trilha inteira em ordem.

  • eventos
  • preventdefault
  • delegacao
  • dom
  • bubbling

Perguntas frequentes

Preciso declarar o parâmetro event para ele existir?
O navegador sempre passa o objeto do evento como primeiro argumento. Você declara o parâmetro para poder usá-lo; se não declarar, ele continua sendo passado, só que sem nome para você acessar dentro da função.
Quando devo usar capture em vez de bubble?
Praticamente nunca em código de aplicação. A captura serve para interceptar um evento antes que um listener de dentro o cancele — típico de biblioteca de terceiros, analytics ou modal que precisa fechar antes de tudo.
stopPropagation resolve o problema de dois listeners disputando?
Resolve o sintoma e cria outro. Ao parar a subida você também apaga o clique para qualquer listener no document, o que quebra fechar-ao-clicar-fora e analytics. Na maioria dos casos, um if no listener de cima é melhor.
Delegação funciona para todo tipo de evento?
Só para os que borbulham. click, input, change, keydown e submit sobem; focus, blur, mouseenter, mouseleave e scroll não. Para focus e blur existem focusin e focusout, que sobem e servem para delegar.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — A interface Event — developer.mozilla.org
  2. MDN — Event: método preventDefault() — developer.mozilla.org
  3. DOM Standard — Dispatching events — dom.spec.whatwg.org

Continue por aqui