Eventos: clique, digitação e envio
Registrar um ouvinte
const botao = document.querySelector("#calcular");
botao.addEventListener("click", () => {
console.log("clicou");
});
Três coisas: o elemento, o nome do evento, e a função que roda quando ele acontece.
Os eventos mais usados
// Clique
botao.addEventListener("click", aoClicar);
// Campo mudou - a cada tecla
campo.addEventListener("input", aoDigitar);
// Campo mudou e a pessoa saiu dele
campo.addEventListener("change", aoMudar);
// Formulário enviado
formulario.addEventListener("submit", aoEnviar);
// Teclado
document.addEventListener("keydown", aoTeclar);
// Foco
campo.addEventListener("focus", aoFocar);
campo.addEventListener("blur", aoSair);
O objeto do evento
A função recebe um objeto com informações sobre o que aconteceu:
campo.addEventListener("input", (event) => {
console.log(event.target.value); // o valor atual
console.log(event.type); // "input"
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") fecharModal();
if (event.key === "Enter" && event.ctrlKey) enviar();
});
event.target é o elemento em que o evento aconteceu.
preventDefault
const formulario = document.querySelector("#inscricao");
formulario.addEventListener("submit", (event) => {
event.preventDefault(); // sem isto, a página recarrega
const dados = new FormData(formulario);
console.log(Object.fromEntries(dados));
});
O comportamento padrão do submit é enviar o formulário e recarregar a página.
Sem preventDefault, o seu código nem termina de rodar.
FormData com Object.fromEntries recolhe todos os campos de uma vez: usando
o name de cada um como chave. É outra razão para todo campo ter name.
Delegação
Cem produtos, cem botões "remover". Cem ouvintes?
// Um ouvinte só, no pai
const lista = document.querySelector("#produtos");
lista.addEventListener("click", (event) => {
const botao = event.target.closest(".remover");
if (!botao) return; // clicou em outra coisa
const item = botao.closest(".produto");
const id = item.dataset.id;
removerProduto(id);
item.remove();
});
O evento sobe do elemento clicado até os ancestrais, e por isso o ouvinte no
pai enxerga o clique no filho. closest descobre em qual filho foi.
Duas vantagens: um ouvinte em vez de cem, e, a que mais importa, funciona para itens criados depois, que um ouvinte registrado no começo nunca alcançaria.
data- para guardar informação
<li class="produto" data-id="acai" data-preco="2500">
Açaí
<button class="remover" aria-label="Remover açaí">✕</button>
</li>
const item = document.querySelector(".produto");
console.log(item.dataset.id); // → "acai"
console.log(Number(item.dataset.preco)); // → 2500
data-preco vira dataset.preco. E é sempre texto: converta antes de usar em
conta.
O teclado não pode ficar de fora
// Ruim: só funciona com mouse
const cartao = document.querySelector(".cartao");
cartao.addEventListener("click", abrirDetalhe);
// Bom: é um botão de verdade
<button class="cartao" type="button">Ver detalhes</button>
Um <button> já recebe foco pelo Tab, já responde ao Enter e ao espaço, e já é
anunciado como botão pelo leitor de tela. Uma <div> com click não faz
nenhuma das três: e para igualar seria preciso tabindex, role e um ouvinte
de teclado, dos quais você vai esquecer pelo menos um.
Uma calculadora inteira
<form id="frete">
<label for="peso">Peso em quilos</label>
<input type="number" id="peso" name="peso" min="0" step="0.1" required />
<label for="destino">Destino</label>
<select id="destino" name="destino">
<option value="tabatinga">Tabatinga</option>
<option value="atalaia">Atalaia do Norte</option>
<option value="manaus">Manaus</option>
</select>
<button type="submit">Calcular</button>
</form>
<p id="resultado" role="status"></p>
const PRECO_POR_KG_EM_CENTAVOS = {
tabatinga: 350,
atalaia: 250,
manaus: 1200,
};
const TAXA_FIXA_EM_CENTAVOS = 500;
const formatar = (centavos) =>
(centavos / 100).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
function calcularFrete(pesoEmKg, destino) {
const precoPorKg = PRECO_POR_KG_EM_CENTAVOS[destino];
if (precoPorKg === undefined) return null;
if (pesoEmKg <= 0) return null;
return Math.ceil(pesoEmKg) * precoPorKg + TAXA_FIXA_EM_CENTAVOS;
}
const formulario = document.querySelector("#frete");
const resultado = document.querySelector("#resultado");
formulario?.addEventListener("submit", (event) => {
event.preventDefault();
const dados = new FormData(formulario);
const peso = Number(dados.get("peso"));
const destino = String(dados.get("destino"));
const freteEmCentavos = calcularFrete(peso, destino);
if (freteEmCentavos === null) {
resultado.textContent = "Informe um peso maior que zero.";
return;
}
resultado.textContent = `Frete para ${peso} kg: ${formatar(freteEmCentavos)}`;
});
Repare em três coisas:
calcularFrete é pura. Ela não conhece a página: recebe dois números e
devolve um. Dá para testá-la sozinha, e é onde a regra de negócio mora.
role="status" no parágrafo do resultado faz o leitor de tela anunciar o
valor quando ele muda. Sem isso, quem não vê a tela não fica sabendo que a conta
foi feita.
A guarda ?. evita o erro caso o formulário não exista naquela página.
Remover um ouvinte
function aoClicar() {
console.log("uma vez só");
}
botao.addEventListener("click", aoClicar);
botao.removeEventListener("click", aoClicar);
// Ou, para um evento único
botao.addEventListener("click", aoClicar, { once: true });
removeEventListener precisa da mesma referência de função: o que não
funciona com arrow escrita direto na chamada.
Exercícios
- Registre um clique num botão que mostre uma mensagem.
- Acompanhe a digitação num campo com
inpute mostre o valor em tempo real. - Trate o
submitde um formulário compreventDefaulteFormData. - Use delegação para tratar cliques em itens de uma lista.
- Guarde um id em
data-ide leia-o pelodataset. - Feche uma caixa com a tecla Escape.
- Monte a calculadora de frete completa, com
role="status"no resultado. - Troque uma
<div>clicável por um<button>e navegue com o teclado.
Referências
- Introdução a eventos - MDN - a introdução
addEventListener- MDN - todas as opçõesFormData- MDN - ler formulário de uma vez
Testa o que você leu
5 perguntas sobre esta aula. Errar aqui não custa nada - a explicação vem junto da correção.