Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EAD

Aula 13 — Funções e eventos

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

📋 Pré-requisitos

Na aula passada você aprendeu a controlar o fluxo do programa (condicionais e laços) e a organizar dados em arrays e objetos — mas tudo aconteceu no console. Hoje o JavaScript sai do console e entra na página: você vai escrever funções reutilizáveis, manipular a árvore do documento e reagir a cliques, teclas e envios de formulário. Na próxima aula, esse mesmo conhecimento valida o formulário de inscrição e cria a busca da programação.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Funções: declaração, expressão, arrow, parâmetros, escopo, funções de ordem superior e boas práticas
2 50 min DOM: seleção, conteúdo, atributos, classes, criação de elementos, navegação e renderização de listas
3 50 min Eventos: modelo, tipos, objeto event, propagação, delegação, debounce; Mão na massa no site do evento

1. Funções

Uma função é um bloco de código com nome, que recebe entradas (parâmetros), faz algo e devolve uma saída (return). Você já usou funções prontas o semestre inteiro — console.log, alert, Number, push — hoje passa a escrever as suas. O motivo é simples: sem funções, todo comportamento repetido vira código copiado e colado, e cada correção precisa ser feita em vários lugares.

Declaração de função

JavaScript
// js/exemplos-funcoes.js
function calcularMedia(a1, a2, a3) {
  return (a1 + a2 + a3) / 3;
}

const media = calcularMedia(8, 7, 9); // 8
console.log(media);

Declarações sofrem hoisting (içamento): o motor JavaScript lê o arquivo inteiro antes de executar e "sobe" as declarações de função para o topo. Por isso é possível chamar calcularMedia em uma linha acima de onde ela foi escrita. Isso é útil para organizar o arquivo com as funções principais no fim e o "programa" no começo.

Expressão de função

JavaScript
const calcularMedia = function (a1, a2, a3) {
  return (a1 + a2 + a3) / 3;
};

Aqui a função é um valor guardado em uma constante. Não sofre hoisting — precisa ser definida antes do uso, como qualquer outra variável.

Arrow function (ES6)

JavaScript
const calcularMedia = (a1, a2, a3) => (a1 + a2 + a3) / 3;

// Um parâmetro: parênteses opcionais
const dobrar = (n) => n * 2;

// Sem parâmetros
const saudacao = () => "Olá!";

// Corpo com várias linhas exige chaves e return explícito
const classificar = (nota) => {
  if (nota >= 6) return "Aprovado";
  if (nota >= 4) return "Exame";
  return "Reprovado";
};

// Retornando um objeto: envolva em parênteses,
// senão as chaves são lidas como corpo da função
const criarAluno = (nome, nota) => ({ nome, nota });

console.log(dobrar(21));                 // 42
console.log(classificar(5.5));           // "Exame"
console.log(criarAluno("Ana", 9.2));     // { nome: "Ana", nota: 9.2 }

⚠️ Atenção Arrow functions não têm this próprio — herdam o do escopo onde foram criadas. Isso as torna ideais como callbacks (funções passadas para forEach, map, addEventListener) e inadequadas como métodos de objeto que precisam acessar o próprio objeto por this.

Forma Hoisting this próprio Quando usar
Declaração function nome() {} Sim Sim Funções principais do arquivo, chamadas de vários lugares
Expressão const nome = function () {} Não Sim Raro em código novo; aparece em código antigo
Arrow const nome = () => {} Não Não (herda) Callbacks, funções curtas, map/filter/reduce

Parâmetros

JavaScript
// Valor padrão: usado quando o argumento não é passado (ou é undefined)
function saudar(nome = "visitante", saudacao = "Olá") {
  return `${saudacao}, ${nome}!`;
}
saudar();                   // "Olá, visitante!"
saudar("Ana", "Bom dia");   // "Bom dia, Ana!"

// Rest (...) no parâmetro: número variável de argumentos vira um array
function somar(...numeros) {
  return numeros.reduce((acumulado, n) => acumulado + n, 0);
}
somar(1, 2, 3, 4, 5);       // 15

// Spread (...) na chamada: "espalha" um array como argumentos separados
const notas = [8, 7, 9];
calcularMedia(...notas);    // o mesmo que calcularMedia(8, 7, 9)

// Objeto como parâmetro: argumentos nomeados, em qualquer ordem
function cadastrar({ nome, email, curso = "Não informado" }) {
  return `${nome} | ${email} | ${curso}`;
}
cadastrar({ email: "a@b.com", nome: "Ana" }); // "Ana | a@b.com | Não informado"

O último padrão — objeto como parâmetro — resolve um problema real: quando a função tem quatro ou cinco parâmetros, ninguém lembra a ordem. Com um objeto, a chamada se autodocumenta.

Escopo

JavaScript
const global = "visível em toda parte";

function exemplo() {
  const local = "só dentro desta função";

  if (true) {
    let bloco = "só dentro deste bloco";
    const tambemBloco = "idem";
    var vazado = "vaza para a função inteira"; // por isso não use var
  }

  console.log(bloco);   // ReferenceError: bloco is not defined
  console.log(vazado);  // funciona — comportamento confuso do var
}

console.log(local);     // ReferenceError: local is not defined

let e const têm escopo de bloco ({}). var tem escopo de função. Regra: cada variável deve existir no menor escopo possível. Se só é usada dentro do if, declare dentro do if.

Funções de ordem superior

Funções que recebem ou retornam outras funções. map, filter e reduce (Aula 12) são exemplos: recebem uma função e a aplicam a cada item. Você também pode criar as suas:

JavaScript
function aplicarDesconto(percentual) {
  return function (preco) {
    return preco * (1 - percentual / 100);
  };
}

const desconto10 = aplicarDesconto(10);
const desconto50 = aplicarDesconto(50);

desconto10(200); // 180
desconto50(200); // 100

aplicarDesconto(10) devolve uma nova função que "lembra" o percentual. Esse "lembrar" tem nome — closure — e é o mecanismo por trás do debounce que você verá na seção 16.

Boas práticas

  1. Uma função, uma responsabilidade. Se o nome tem "e" (salvarEEnviar), provavelmente são duas funções.
  2. Nomes são verbos: calcularMedia, validarEmail, formatarData, renderizarLista.
  3. Evite mais de 3–4 parâmetros. Acima disso, receba um objeto.
  4. Prefira retornar valores a alterar variáveis externas. Funções puras (mesma entrada → mesma saída, sem efeitos colaterais) são fáceis de testar no console.
  5. Retorne cedo para reduzir aninhamento:
JavaScript
// Em vez de aninhar três ifs
function processar(dado) {
  if (!dado) return null;
  if (!dado.ativo) return null;
  return dado.valor * 2;
}

2. O que é o DOM

O DOM (Document Object Model) é a representação em memória do documento, montada pelo navegador ao interpretar o HTML. Ele expõe o documento como uma árvore de objetos que o JavaScript pode ler e modificar.

HTML
<!-- exemplo.html -->
<html>
  <body>
    <h1 id="titulo">Olá</h1>
    <ul class="lista">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  </body>
</html>
Texto
document
└── html
    └── body
        ├── h1#titulo
        │   └── "Olá"              (nó de texto)
        └── ul.lista
            ├── li → "Item 1"
            └── li → "Item 2"

Três verdades importantes:

  1. O DOM não é o HTML. O HTML é o texto-fonte; o DOM é a estrutura viva em memória. Se o JavaScript adicionar elementos, eles existem no DOM sem existir no arquivo .html. Por isso "Exibir código-fonte" (Ctrl+U) mostra o arquivo original, enquanto a aba Elements do DevTools mostra o DOM atual.
  2. Alterar o DOM altera a tela imediatamente. Não há "salvar" nem "atualizar".
  3. Recarregar a página descarta tudo. Toda alteração feita por JavaScript some — a menos que seja persistida (você verá localStorage na próxima aula).

O objeto de entrada é document, que é filho de window (o objeto global do navegador — alert, setTimeout e location também vivem nele).

🧠 Você sabia? O DOM nasceu de uma guerra. Em 1997, Netscape Navigator 4 e Internet Explorer 4 lançaram, cada um, seu próprio modelo de objetos para "HTML dinâmico" — document.layers de um lado, document.all do outro — e o mesmo script não rodava nos dois. Programadores escreviam tudo em dobro. O W3C publicou o DOM Level 1 em 1998 para acabar com isso, e hoje o padrão é mantido pelo WHATWG como o DOM Living Standard. O querySelector que você vai usar só chegou em 2008, com a especificação Selectors API.

3. Seleção de elementos

Métodos modernos — use estes

JavaScript
// Retorna o PRIMEIRO elemento que casa com o seletor CSS (ou null)
const titulo = document.querySelector("#titulo");
const primeiroItem = document.querySelector(".lista li");
const botao = document.querySelector("button.primario");
const campo = document.querySelector("input[type='email']");

// Retorna TODOS os que casam, como NodeList (mesmo que vazia)
const itens = document.querySelectorAll(".lista li");
const paragrafos = document.querySelectorAll("article p");

querySelector e querySelectorAll aceitam qualquer seletor CSS — combinadores, pseudoclasses, seletores de atributo. É por isso que a Aula 06 é pré-requisito desta.

Métodos clássicos

JavaScript
document.getElementById("titulo");          // sem #
document.getElementsByClassName("lista");   // HTMLCollection AO VIVO
document.getElementsByTagName("li");        // HTMLCollection AO VIVO
document.getElementsByName("turno");        // NodeList
Característica querySelectorAll getElementsBy*
Retorna NodeList estática HTMLCollection ao vivo
Reflete mudanças posteriores no DOM Não Sim
Tem forEach Sim Não (precisa converter)
Aceita seletor CSS Sim Não

⚠️ Atenção A coleção "ao vivo" é uma fonte clássica de bugs: se você remove elementos dentro de um laço que percorre uma HTMLCollection, a coleção encolhe durante a iteração e você pula elementos. Prefira querySelectorAll.

Percorrendo o resultado

JavaScript
const itens = document.querySelectorAll("li");

itens.forEach((item) => console.log(item.textContent));

// Se precisar de métodos de array (map, filter, find), converta:
const textos = Array.from(itens).map((i) => i.textContent);
const textos2 = [...itens].map((i) => i.textContent);

⚠️ Atenção querySelector retorna null quando não encontra. Chamar algo em null gera TypeError: Cannot read properties of null (reading 'addEventListener'). Este é o erro mais frequente de quem está aprendendo DOM. Suas duas causas quase sempre são: (a) seletor errado (#menu quando o id é menu-principal); (b) script rodando antes de o HTML existir — resolvido com defer na tag <script>.

🔬 Investigue Abra qualquer página, vá à aba Elements do DevTools e clique em um elemento. Volte ao Console e digite $0 — o navegador devolve o elemento selecionado. Agora experimente $0.textContent, $0.classList, $0.parentElement e $0.style.outline = "3px solid red". $0 é o último elemento clicado, $1 o anterior, e assim por diante. É a forma mais rápida de testar seletores: $$(".card") equivale a document.querySelectorAll(".card").

4. Lendo e alterando conteúdo

JavaScript
const titulo = document.querySelector("#titulo");

// LEITURA
titulo.textContent; // texto puro, incluindo o de elementos ocultos
titulo.innerText;   // texto como renderizado (respeita CSS, mais lento)
titulo.innerHTML;   // conteúdo HTML como string

// ESCRITA
titulo.textContent = "Novo título";
titulo.innerHTML = "Novo <strong>título</strong>";
Propriedade Interpreta HTML? Uso
textContent Não — tags viram texto literal Padrão. Use sempre que possível
innerText Não Quando importa o texto visível (respeita display: none)
innerHTML Sim Só quando você realmente precisa inserir marcação

⚠️ Atenção — XSS Nunca faça elemento.innerHTML = dadoDoUsuario. Se o usuário digitar <img src=x onerror="alert(document.cookie)"> em um campo e você inserir isso com innerHTML, o código dele executa na sua página — para todo mundo que abrir. Isso se chama Cross-Site Scripting (XSS) e é uma das vulnerabilidades mais exploradas da Web. Regra: dado vindo de usuário → textContent. Marcação que você mesmo escreveu → innerHTML é aceitável.

5. Atributos

JavaScript
const link = document.querySelector("a");

link.getAttribute("href");
link.setAttribute("href", "https://www.wikipedia.org");
link.setAttribute("target", "_blank");
link.hasAttribute("download"); // true/false
link.removeAttribute("target");

// Propriedades diretas para atributos comuns
link.href;
link.id;
img.src;
img.alt;
input.value;           // valor atual do campo
input.checked;         // true/false para checkbox/radio
input.disabled = true;
select.value;

💡 Dica input.value é diferente de input.getAttribute("value"). O primeiro é o valor atual (o que o usuário digitou); o segundo é o valor inicial escrito no HTML. Ao trabalhar com formulários, use sempre .value.

data-* — atributos personalizados

HTML
<button data-id="42" data-acao="excluir" data-nome-produto="Notebook">
  Excluir
</button>
JavaScript
const btn = document.querySelector("button");

btn.dataset.id;          // "42" (sempre string!)
btn.dataset.acao;        // "excluir"
btn.dataset.nomeProduto; // "Notebook" — data-nome-produto vira nomeProduto (camelCase)

btn.dataset.status = "ativo"; // cria data-status="ativo" no HTML

Atributos data-* são a forma padrão de guardar informação de aplicação no HTML. Serão essenciais na delegação de eventos (seção 14) e no CRUD do Boss da Unidade: cada botão de editar/excluir carrega o data-id do registro.

6. Classes e estilos

classList — a forma correta

JavaScript
const card = document.querySelector(".card");

card.classList.add("ativo");
card.classList.add("destaque", "novo");   // várias de uma vez
card.classList.remove("ativo");
card.classList.toggle("aberto");          // adiciona se não tem, remove se tem
card.classList.toggle("aberto", condicao); // força adicionar (true) ou remover (false)
card.classList.contains("ativo");         // true/false
card.classList.replace("antigo", "novo");

⚠️ Atenção Evite card.className = "novo" — isso substitui todas as classes existentes de uma vez.

Estilos inline

JavaScript
card.style.backgroundColor = "#0b3d5c"; // camelCase, não background-color
card.style.fontSize = "18px";           // sempre com unidade
card.style.display = "none";
card.style.setProperty("--cor-tema", "#e74c3c"); // variáveis CSS

// Leitura do estilo COMPUTADO (o que realmente está aplicado, vindo de qualquer folha)
const estilo = getComputedStyle(card);
estilo.backgroundColor;
estilo.width;

💡 Dica Prefira trocar classes a manipular style diretamente. O CSS fica no CSS, o comportamento fica no JS, e você mantém a separação de responsabilidades que construiu na Unidade 2.

JavaScript
// Espalha estilo pelo JavaScript
el.style.display = "none";
el.style.opacity = "0";

// O CSS define o que é "oculto"; o JS só decide quando
el.classList.add("oculto");
CSS
/* css/estilo.css */
.oculto {
  display: none;
}

7. Criando e inserindo elementos

JavaScript
// 1. Criar
const li = document.createElement("li");

// 2. Configurar
li.textContent = "Novo item";
li.classList.add("item");
li.dataset.id = "7";

// 3. Inserir
const lista = document.querySelector("ul");
lista.appendChild(li);               // no fim
lista.append(li, outroLi);           // no fim, aceita vários nós e texto
lista.prepend(li);                   // no início
lista.insertBefore(li, referencia);  // antes de um filho específico

// Inserção posicional em relação a um elemento
elemento.insertAdjacentElement("beforebegin", novo); // antes do elemento
elemento.insertAdjacentElement("afterbegin", novo);  // primeiro filho
elemento.insertAdjacentElement("beforeend", novo);   // último filho
elemento.insertAdjacentElement("afterend", novo);    // depois do elemento

elemento.insertAdjacentHTML("beforeend", "<li>Item</li>"); // com string HTML

Removendo e substituindo

JavaScript
elemento.remove();              // remove a si mesmo
pai.removeChild(filho);         // forma antiga
pai.replaceChild(novo, antigo);
elemento.replaceWith(novo);

lista.innerHTML = "";           // esvazia (rápido e comum)

Clonando

JavaScript
const copia = elemento.cloneNode(true); // true = com descendentes

8. Navegação entre nós

JavaScript
const item = document.querySelector(".item");

// Para cima
item.parentElement;
item.closest(".card"); // ancestral mais próximo que casa com o seletor (inclui o próprio)

// Para baixo
item.children;           // HTMLCollection de elementos filhos
item.firstElementChild;
item.lastElementChild;
item.querySelector(".titulo"); // busca DENTRO do item

// Para os lados
item.nextElementSibling;
item.previousElementSibling;

⚠️ Atenção Existem versões sem Element no nome (parentNode, firstChild, nextSibling) que consideram nós de texto — inclusive espaços e quebras de linha do seu HTML. Isso surpreende: ul.firstChild costuma ser um nó de texto vazio, não o primeiro <li>. Use sempre as versões com Element.

closest() é indispensável: em uma lista, ao clicar num botão dentro de um card, btn.closest(".card") devolve o card inteiro — mesmo que o clique tenha sido em um ícone dentro do botão.

9. Renderizando listas a partir de dados

Este é o padrão central do desenvolvimento front-end e será usado em todas as aulas seguintes: os dados vivem em um array; uma função desenha a tela a partir dele. Quando os dados mudam, chama-se a função de novo.

JavaScript
// js/produtos.js
const produtos = [
  { id: 1, nome: "Notebook", preco: 3500, categoria: "Informática" },
  { id: 2, nome: "Mouse", preco: 80, categoria: "Periféricos" },
  { id: 3, nome: "Teclado", preco: 150, categoria: "Periféricos" },
];

const container = document.querySelector("#lista-produtos");

function renderizar(lista) {
  container.innerHTML = ""; // 1. limpa

  if (lista.length === 0) {  // 2. estado vazio
    const aviso = document.createElement("p");
    aviso.classList.add("vazio");
    aviso.textContent = "Nenhum produto encontrado.";
    container.appendChild(aviso);
    return;
  }

  lista.forEach((produto) => { // 3. cria um nó por item
    const card = document.createElement("article");
    card.classList.add("card-produto");
    card.dataset.id = produto.id;

    const titulo = document.createElement("h3");
    titulo.textContent = produto.nome; // textContent: seguro contra XSS

    const preco = document.createElement("p");
    preco.classList.add("preco");
    preco.textContent = produto.preco.toLocaleString("pt-BR", {
      style: "currency",
      currency: "BRL",
    });

    const btn = document.createElement("button");
    btn.type = "button";
    btn.textContent = "Excluir";
    btn.dataset.id = produto.id;
    btn.classList.add("btn-excluir");

    card.append(titulo, preco, btn);
    container.appendChild(card);
  });
}

renderizar(produtos);

Versão com template literal

Mais concisa, porém exige cuidado com XSS se os dados vierem do usuário:

JavaScript
function renderizar(lista) {
  container.innerHTML = lista
    .map(
      (p) => `
      <article class="card-produto" data-id="${p.id}">
        <h3>${p.nome}</h3>
        <p class="preco">R$ ${p.preco.toFixed(2)}</p>
        <button type="button" class="btn-excluir" data-id="${p.id}">Excluir</button>
      </article>
    `
    )
    .join("");
}

Se p.nome veio de um campo digitado por alguém, essa versão é vulnerável. Uma saída intermediária: montar a marcação fixa com template literal e preencher os textos variáveis com textContent depois.

Performance: DocumentFragment

Cada appendChild direto no documento pode disparar um reflow (recálculo de layout). Para inserir muitos elementos, monte fora do documento e insira uma vez só:

JavaScript
const fragmento = document.createDocumentFragment();
lista.forEach((item) => {
  const li = document.createElement("li");
  li.textContent = item.nome;
  fragmento.appendChild(li); // ainda fora do documento
});
container.appendChild(fragmento); // uma única operação no DOM

🔎 Por baixo do capô O navegador não redesenha a tela a cada linha de JavaScript — ele acumula mudanças e recalcula o layout quando precisa (ou quando você lê uma medida, como offsetHeight, forçando o cálculo). Mesmo assim, inserir 500 nós um a um é mais caro que inserir um fragmento com 500 nós. Para listas de até algumas dezenas de itens, a diferença é imperceptível; para centenas, ela aparece no celular.

10. O modelo de eventos

Um evento é um sinal de que algo aconteceu: um clique, uma tecla, o envio de um formulário, o fim do carregamento. O JavaScript no navegador é orientado a eventos: você registra funções (ouvintes ou listeners) que serão chamadas quando o evento ocorrer. Entre um evento e outro, seu código não faz nada — o navegador fica esperando.

O mínimo para começar

JavaScript
const botao = document.querySelector("#meu-botao");

botao.addEventListener("click", function (evento) {
  console.log("Clicado!", evento.target);
});

// Com arrow function
botao.addEventListener("click", () => {
  document.querySelector("#saida").textContent = "Você clicou!";
});

// Garantir que o DOM está pronto (desnecessário se o script usa defer)
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM montado");
});

As três formas (e a única correta)

HTML
<!-- 1. Atributo HTML — evite: mistura comportamento com estrutura -->
<button onclick="alert('Oi')">Clique</button>
JavaScript
// 2. Propriedade do elemento — permite apenas UM ouvinte por evento
botao.onclick = function () { console.log("A"); };
botao.onclick = function () { console.log("B"); }; // sobrescreve o anterior!

// 3. addEventListener — a forma correta
botao.addEventListener("click", function () { console.log("A"); });
botao.addEventListener("click", function () { console.log("B"); });
// Ambos executam, na ordem de registro

🧠 Você sabia? addEventListener só passou a funcionar em todos os navegadores em 2009, com o Internet Explorer 9. Até lá, o IE usava um método próprio, attachEvent, com outra assinatura e outro comportamento de this. Uma das razões de a biblioteca jQuery ter dominado a Web por uma década foi justamente esconder essa diferença atrás de um .on("click", fn) único. Hoje o padrão venceu e o jQuery é desnecessário — mas você ainda encontrará onclick e attachEvent em código legado.

Sintaxe completa

JavaScript
elemento.addEventListener(tipo, funcao, opcoes);

// Exemplos
botao.addEventListener("click", tratarClique);
botao.addEventListener("click", () => console.log("Oi"));

botao.addEventListener("click", tratarClique, {
  once: true,     // executa apenas uma vez e se remove
  capture: false, // fase de captura em vez de borbulhamento (seção 13)
  passive: true,  // promete não chamar preventDefault (melhora a rolagem no celular)
});

// Remoção — exige a MESMA referência de função
function tratarClique() { console.log("Oi"); }
botao.addEventListener("click", tratarClique);
botao.removeEventListener("click", tratarClique); // funciona

botao.addEventListener("click", () => {});
botao.removeEventListener("click", () => {});     // não remove nada

⚠️ Atenção Funções anônimas não podem ser removidas, porque cada arrow function escrita é um objeto novo — a segunda () => {} não é a mesma que a primeira. Se precisar remover depois, nomeie a função.

⚠️ Atenção — o erro mais comum de todos botao.addEventListener("click", tratarClique()) — com parênteses — chama a função imediatamente, no momento do registro, e passa o retorno dela (geralmente undefined) como ouvinte. O clique depois não faz nada. Passe a referência: tratarClique, sem parênteses. Se precisar passar argumentos, envolva em uma arrow: () => tratarClique(42).

11. Principais tipos de evento

Mouse

Evento Quando ocorre
click Clique (pressionar e soltar sobre o elemento); também disparado por Enter/Espaço em botões e por toque
dblclick Duplo clique
mousedown / mouseup Botão pressionado / solto
mouseenter / mouseleave Entra / sai do elemento. Não borbulham
mouseover / mouseout Idem, mas borbulham dos filhos
mousemove Movimento do ponteiro (dispara dezenas de vezes por segundo)
contextmenu Botão direito

Teclado

Evento Quando ocorre
keydown Tecla pressionada (repete se segurar)
keyup Tecla solta
keypress Obsoleto — não use
JavaScript
campo.addEventListener("keydown", (e) => {
  console.log(e.key);  // "a", "Enter", "Escape", "ArrowUp", " "
  console.log(e.code); // "KeyA", "Enter", "Space" — posição física da tecla
  console.log(e.ctrlKey, e.shiftKey, e.altKey);

  if (e.key === "Enter") enviar();
  if (e.key === "Escape") fecharModal();
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault(); // impede o "Salvar página" do navegador
    salvar();
  }
});

💡 Dica Use e.key para saber qual caractere; use e.code para saber qual tecla física (importante em jogos e em teclados de layouts diferentes — no teclado ABNT2, e.key da tecla ao lado do Enter é "ç", mas e.code é "Semicolon").

Formulário

Evento Quando ocorre
submit Formulário enviado — registre no <form>, não no botão
input A cada alteração do valor. O mais útil para validação ao vivo
change Ao perder o foco após alterar (ou imediatamente em select/checkbox/radio)
focus / blur Ganha / perde foco. Não borbulham
focusin / focusout Idem, mas borbulham
reset Formulário limpo

Janela e documento

Evento Quando ocorre
DOMContentLoaded HTML lido e DOM montado (não espera imagens)
load Tudo carregado, inclusive imagens e estilos
resize Janela redimensionada
scroll Rolagem
beforeunload Antes de sair da página

Toque

touchstart, touchmove, touchend. Em geral, click já funciona em telas de toque — só use eventos de toque para gestos específicos (arrastar, pinçar).

🔬 Investigue No Console, digite monitorEvents(document.body, "click") e clique em qualquer lugar da página: cada clique é impresso com o objeto do evento. Troque para monitorEvents(document.body, ["keydown", "keyup"]) e digite algo. Quando cansar, unmonitorEvents(document.body). Depois selecione um botão na aba Elements e abra a sub-aba Event Listeners no painel lateral: ela lista todos os ouvintes registrados naquele elemento e nos ancestrais — com o arquivo e a linha onde cada um foi escrito. É assim que você descobre "quem está reagindo a esse clique" em um site que não escreveu.

12. O objeto event

Toda função ouvinte recebe automaticamente um objeto com informações sobre o que ocorreu.

JavaScript
elemento.addEventListener("click", function (event) {
  event.type;          // "click"
  event.target;        // elemento que ORIGINOU o evento
  event.currentTarget; // elemento onde o OUVINTE está registrado
  event.timeStamp;     // milissegundos desde o carregamento da página

  event.clientX, event.clientY; // coordenadas na janela
  event.pageX, event.pageY;     // coordenadas na página (com rolagem)

  event.preventDefault();  // cancela o comportamento padrão
  event.stopPropagation(); // interrompe a propagação
});

⚠️ Atenção — target × currentTarget Se você clica em um <span> dentro de um <button> que tem o ouvinte, target é o <span> e currentTarget é o <button>. Confundir os dois é a causa mais comum de bugs em delegação de eventos.

preventDefault()

JavaScript
// Impede o envio e o recarregamento da página
formulario.addEventListener("submit", (e) => {
  e.preventDefault();
  processarDados();
});

// Impede a navegação de um link
link.addEventListener("click", (e) => {
  e.preventDefault();
  abrirModal();
});

// Bloqueia caracteres não numéricos (teclas de um só caractere)
campo.addEventListener("keydown", (e) => {
  if (!/[0-9]/.test(e.key) && e.key.length === 1) e.preventDefault();
});

Sem e.preventDefault() no submit, a página recarrega e todo o seu JavaScript é reiniciado — os dados somem e parece que "o código não funcionou". É o erro nº 1 da próxima aula.

13. Propagação de eventos

Quando você clica em um elemento aninhado, o evento percorre a árvore em três fases:

Texto
                ┌── document ──┐
   CAPTURA      │              │     BORBULHAMENTO
   (descendo)   ▼              ▲     (subindo)
              <div>          <div>
                ▼              ▲
               <ul>          <ul>
                ▼              ▲
               <li>          <li>
                ▼              ▲
            <button> ── ALVO ──┘
  1. Captura: de document até o alvo.
  2. Alvo: no elemento clicado.
  3. Borbulhamento (bubbling): do alvo de volta até document. É a fase padrão — onde os ouvintes ficam, a menos que você peça o contrário.
JavaScript
document.querySelector("ul").addEventListener("click", () => console.log("UL"));
document.querySelector("li").addEventListener("click", () => console.log("LI"));
// Clicar no LI imprime: "LI", depois "UL"

// Para ouvir na captura:
ul.addEventListener("click", tratar, true);
// ou { capture: true } — agora imprime "UL", depois "LI"
JavaScript
event.stopPropagation();          // impede que continue subindo
event.stopImmediatePropagation(); // também impede outros ouvintes no MESMO elemento

⚠️ Atenção Use stopPropagation com parcimônia. Ele quebra a delegação e cria comportamentos difíceis de rastrear meses depois ("por que o menu não fecha quando clico aqui?"). Antes de usá-lo, pergunte se não é melhor verificar event.target.

14. Delegação de eventos

O problema: você adiciona ouvintes a 50 botões de "excluir". Depois, o JavaScript cria mais 10 botões dinamicamente. Os novos não têm ouvinte — porque addEventListener só afeta elementos que existiam no momento do registro.

A solução: registre um único ouvinte no contêiner pai e descubra, pelo event.target, quem foi realmente clicado. Isso funciona porque os eventos borbulham.

JavaScript
const lista = document.querySelector("#lista-produtos");

lista.addEventListener("click", (event) => {
  // Encontra o botão, mesmo se o clique foi num ícone dentro dele
  const botaoExcluir = event.target.closest(".btn-excluir");
  if (!botaoExcluir) return; // clique fora dos botões: ignora

  const id = Number(botaoExcluir.dataset.id);
  excluirProduto(id);
});

Vantagens:

  1. Funciona para elementos criados depois — sem re-registrar nada.
  2. Um ouvinte em vez de N — menos memória, melhor desempenho.
  3. Código mais simples — a lógica fica concentrada em um lugar.

Padrão para múltiplas ações no mesmo contêiner

HTML
<button type="button" data-acao="editar" data-id="7">Editar</button>
<button type="button" data-acao="excluir" data-id="7">Excluir</button>
JavaScript
lista.addEventListener("click", (e) => {
  const botao = e.target.closest("button[data-acao]");
  if (!botao) return;

  const { acao, id } = botao.dataset;

  switch (acao) {
    case "editar":
      editar(Number(id));
      break;
    case "excluir":
      excluir(Number(id));
      break;
    case "concluir":
      concluir(Number(id));
      break;
  }
});

Este padrão é a espinha dorsal de qualquer CRUD em JavaScript puro — e vai aparecer de novo no Marco 3. Vale a pena entendê-lo agora.

15. Eventos personalizados

Você também pode criar os seus próprios eventos, com dados anexados em detail:

JavaScript
const evento = new CustomEvent("produtoAdicionado", {
  detail: { id: 5, nome: "Mouse" },
  bubbles: true,
});

document.dispatchEvent(evento);

document.addEventListener("produtoAdicionado", (e) => {
  console.log(e.detail.nome); // "Mouse"
});

Útil para desacoplar partes da aplicação: um módulo avisa que algo aconteceu sem conhecer quem vai reagir. No site do evento, por exemplo, o formulário de inscrição pode disparar inscricaoConfirmada e o contador de vagas, em outro arquivo, apenas escuta.

16. Controle de frequência: debounce e throttle

Eventos como input, scroll, mousemove e resize disparam dezenas de vezes por segundo. Executar uma operação pesada (filtrar 500 itens, redesenhar uma lista) em cada disparo trava a interface.

JavaScript
// DEBOUNCE — só executa depois que os disparos PARAM por X ms
function debounce(fn, atraso = 300) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), atraso);
  };
}

const buscar = debounce((texto) => {
  console.log("Buscando por:", texto);
}, 400);

campoBusca.addEventListener("input", (e) => buscar(e.target.value));
JavaScript
// THROTTLE — executa no máximo uma vez a cada X ms
function throttle(fn, intervalo = 200) {
  let podeExecutar = true;
  return function (...args) {
    if (!podeExecutar) return;
    podeExecutar = false;
    fn.apply(this, args);
    setTimeout(() => {
      podeExecutar = true;
    }, intervalo);
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("Rolando");
  }, 200)
);
Aspecto Debounce Throttle
Quando executa Após a pausa nos disparos Em intervalos regulares
Uso típico Busca ao digitar, validação, redimensionamento Rolagem, movimento do mouse, animações

🔎 Por baixo do capô debounce só funciona por causa das closures da seção 1: a variável timer fica "presa" dentro da função retornada e sobrevive entre uma chamada e outra. Cada tecla cancela o setTimeout anterior e agenda outro; só o último sobrevive 300 ms e executa fn.

17. Acessibilidade em eventos

JavaScript
// Só funciona com mouse
div.addEventListener("click", acao);

// Use um <button> de verdade: já é focável, acionável por Enter/Espaço
// e anunciado como botão pelo leitor de tela
botao.addEventListener("click", acao);

Se por algum motivo precisar tornar um elemento genérico interativo:

HTML
<div role="button" tabindex="0" aria-pressed="false">Alternar</div>
JavaScript
div.addEventListener("keydown", (e) => {
  if (e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    acao();
  }
});

Regra: se algo é clicável, use <button>. Se navega para outro lugar, use <a href>. Reinventar controles com <div> é a origem da maioria dos problemas de acessibilidade em sites modernos — e o Lighthouse (Aula 15) penaliza isso.

📌 Vale gravar Vale saber de cor: (1) a diferença entre target e currentTarget; (2) por que addEventListener("click", fn()) não funciona; (3) o que acontece sem preventDefault no submit; (4) por que delegação resolve elementos criados depois; (5) a ordem de impressão em um exemplo de borbulhamento.

18. A arquitetura: estado → dados → renderização → eventos

Antes de ir para o projeto, um esqueleto que vale para qualquer aplicação front-end — do menu de hoje ao CRUD do fim da unidade:

JavaScript
// Esqueleto genérico de organização — não é o js/app.js do projeto.
// Os nomes abaixo (els, renderizar, tarefas) são de exemplo; no seu código,
// use nomes específicos, porque scripts sem type="module" dividem o mesmo
// escopo global e nomes genéricos colidem entre arquivos.
// ===== ESTADO: a fonte única da verdade =====
let tarefas = [];
let filtroAtual = "todas";

// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====
const els = {
  form: document.querySelector("#form-tarefa"),
  input: document.querySelector("#input-tarefa"),
  lista: document.querySelector("#lista-tarefas"),
};

// ===== FUNÇÕES DE DADOS (puras: alteram só o estado) =====
function adicionarTarefa(texto) {
  tarefas.push({ id: Date.now(), texto, concluida: false });
}

function alternarConclusao(id) {
  const tarefa = tarefas.find((t) => t.id === id);
  if (tarefa) tarefa.concluida = !tarefa.concluida;
}

function obterTarefasVisiveis() {
  if (filtroAtual === "pendentes") return tarefas.filter((t) => !t.concluida);
  if (filtroAtual === "concluidas") return tarefas.filter((t) => t.concluida);
  return [...tarefas];
}

// ===== RENDERIZAÇÃO (desenha a tela a partir do estado) =====
function renderizar() {
  els.lista.innerHTML = "";
  obterTarefasVisiveis().forEach((t) => {
    const li = document.createElement("li");
    li.dataset.id = t.id;
    li.classList.toggle("concluida", t.concluida);
    li.textContent = t.texto;
    els.lista.appendChild(li);
  });
}

// ===== EVENTOS (capturam a intenção do usuário) =====
function registrarEventos() {
  els.form.addEventListener("submit", (e) => {
    e.preventDefault();
    const texto = els.input.value.trim();
    if (texto === "") return;
    adicionarTarefa(texto);
    els.input.value = "";
    renderizar();
  });

  els.lista.addEventListener("click", (e) => {
    const li = e.target.closest("li[data-id]");
    if (!li) return;
    alternarConclusao(Number(li.dataset.id));
    renderizar();
  });
}

// ===== INICIALIZAÇÃO =====
function iniciar() {
  registrarEventos();
  renderizar();
}

iniciar();

O fluxo é sempre o mesmo: usuário age → evento → função altera o estado → renderizar() redesenha. Nunca altere o DOM "na mão" para refletir uma mudança de dado: se o usuário conclui uma tarefa, não risque o <li> — mude o array e renderize de novo. Quando DOM e dados divergem, os bugs se tornam impossíveis de rastrear. Frameworks como Vue e React (Nível 3) apenas automatizam essa mesma ideia.

💻 Mão na massa — O menu que você já tinha, agora entendido, e a lista de palestrantes

O site do evento já tem um menu hambúrguer que abre e fecha desde a Aula 08, com oito linhas de JavaScript que você colou "sem entender", e uma página de palestrantes escrita à mão, artigo por artigo. Hoje o js/menu.js cresce com o que você aprendeu (fechar com Esc, fechar ao clicar fora, reagir ao resize), o js/app.js ganha uma seção nova sem perder nada do que já fazia, e a página de palestrantes passa a ser renderizada a partir do js/dados.js da Aula 12, com filtro por área usando delegação.

⚠️ Atenção Três coisas que não acontecem hoje: (1) a marcação do cabeçalho não muda — o <button class="menu-botao"> com o <span class="menu-botao__icone"> e a <ul id="menu-principal" class="menu"> são os da Aula 08, com o .menu__cta de "Inscreva-se" no fim; (2) o CSS do menu não muda — a abertura continua sendo a animação de opacity + visibility + transform da Aula 09, sem display: none; (3) nenhum arquivo é apagado nem reescrito do zero. menu.js, efeitos.js, app.js, dados.js e relatorios.js continuam existindo, cada um com a sua responsabilidade.

Passo 1 — Reler e ampliar o js/menu.js

Abra js/menu.js. Ele tem isto desde a Aula 08:

site-evento/js/menu.js (como está)

JavaScript
// Seleciona o botão e a lista pelo seletor CSS
const botao = document.querySelector('.menu-botao');
const menu = document.querySelector('#menu-principal');

// A cada clique, inverte o valor de aria-expanded
botao.addEventListener('click', () => {
  const aberto = botao.getAttribute('aria-expanded') === 'true';
  botao.setAttribute('aria-expanded', String(!aberto));
});

Agora você consegue ler cada linha: document.querySelector é a seleção da seção 3; addEventListener('click', …) é o registro de tratador da seção 10; a arrow function é a forma da seção 1. E consegue ver o que falta — as três melhorias que ficaram como exercício B3 da Aula 08.

Substitua o conteúdo do arquivo por esta versão. Os nomes botao e menu continuam os mesmos: eles pertencem a este arquivo e a mais nenhum. Se você declarar um const menu também no app.js, o navegador para tudo com Identifier 'menu' has already been declared — dois <script> sem type="module" compartilham o mesmo escopo global.

site-evento/js/menu.js (versão desta aula)

JavaScript
// menu.js — comportamento do menu hambúrguer (marcação e CSS: Aulas 08 e 09)

// ===== ELEMENTOS =====
const botao = document.querySelector(".menu-botao");
const menu = document.querySelector("#menu-principal");
const LARGURA_DESKTOP = 768; // o mesmo ponto de quebra do CSS da Aula 08

// ===== FUNÇÕES DE ESTADO =====
// O estado mora no atributo aria-expanded do botão: uma fonte só, que o CSS
// também lê (.menu-botao[aria-expanded="true"] + .menu). Nada de classe .aberto.
function menuEstaAberto() {
  return botao.getAttribute("aria-expanded") === "true";
}

function definirMenu(aberto) {
  botao.setAttribute("aria-expanded", String(aberto));
}

function fecharMenu() {
  definirMenu(false);
}

function alternarMenu() {
  definirMenu(!menuEstaAberto());
}

// ===== EVENTOS =====
function registrarEventosDoMenu() {
  // 1. Clique no botão: abre ou fecha
  botao.addEventListener("click", alternarMenu);

  // 2. Esc fecha e devolve o foco ao botão (seção 17: nunca perca o foco)
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && menuEstaAberto()) {
      fecharMenu();
      botao.focus();
    }
  });

  // 3. Clique fora do menu e do botão: fecha (um ouvinte só, no document)
  document.addEventListener("click", (e) => {
    const clicouDentro = menu.contains(e.target) || botao.contains(e.target);
    if (!clicouDentro && menuEstaAberto()) {
      fecharMenu();
    }
  });

  // 4. Ao alargar a janela para desktop, garante o estado fechado
  window.addEventListener("resize", () => {
    if (window.innerWidth >= LARGURA_DESKTOP && menuEstaAberto()) {
      fecharMenu();
    }
  });
}

// ===== INICIALIZAÇÃO =====
// Se uma página não tiver cabeçalho (a 404.html da Aula 15, por exemplo),
// querySelector devolve null e o script não deve quebrar.
if (botao && menu) {
  registrarEventosDoMenu();
}

Três observações:

Passo 2 — Acrescentar ao js/app.js (sem apagar nada)

O js/app.js já tem, das Aulas 10 e 11: as constantes do evento, a saudação no console, o console.group, o console.table das cinco páginas e a contagem regressiva do index.html. Nada disso sai. Você só organiza o arquivo em blocos e acrescenta uma seção no fim.

Comece pondo os comentários de seção sobre o que já existe, na ordem da seção 18 desta aula:

site-evento/js/app.js (topo do arquivo, comentários acrescentados)

JavaScript
// app.js — comportamento comum a todas as páginas do site do evento

// ===== ESTADO =====
const NOME_EVENTO = "Semana Acadêmica de Sistemas de Informação";
const EDICAO = 12;
const LOCAL_EVENTO = "Auditório Central";
const TRILHAS = "Desenvolvimento Web, Dados, Segurança";

Deixe a saudação, o console.group, o console.table e todo o bloco ===== CONTAGEM REGRESSIVA PARA A ABERTURA ===== exatamente onde estão. Depois, no fim do arquivo, acrescente:

site-evento/js/app.js (acrescente ao fim)

JavaScript
// ===== NAVEGAÇÃO: marcar a página atual =====
/**
 * Marca com aria-current="page" o link do menu que aponta para a página aberta.
 * @param {string} [padrao="index.html"] - arquivo assumido quando a URL termina em "/"
 */
function marcarPaginaAtual(padrao = "index.html") {
  const listaDoMenu = document.querySelector("#menu-principal");
  if (listaDoMenu === null) return;

  const arquivoAtual = location.pathname.split("/").pop() || padrao;

  listaDoMenu.querySelectorAll("a").forEach((link) => {
    const destino = link.getAttribute("href");
    if (destino === arquivoAtual) {
      link.setAttribute("aria-current", "page");
    } else {
      link.removeAttribute("aria-current");
    }
  });
}

marcarPaginaAtual();

⚠️ Atenção Isto é uma troca declarada, não um acréscimo: até a Aula 08 o aria-current="page" era escrito à mão no HTML de cada página. Agora ele passa a ser aplicado pelo JavaScript, então apague o atributo das cinco páginas — se você deixar os dois, um link renomeado vai ficar marcado em dois lugares e a função vai remover o atributo que o HTML afirmava. Uma informação, uma fonte.

O preço dessa troca é real e você precisa conhecê-lo: sem JavaScript, o destaque da página atual some. Ele é aceitável aqui porque aria-current é reforço — o menu continua navegável, os links continuam funcionando e o <title> continua dizendo onde a pessoa está. Se o atributo fosse a única forma de saber a página atual, o certo seria mantê-lo no HTML.

Passo 3 — A página de palestrantes renderizada a partir do dados.js

O js/dados.js da Aula 12 já tem o array palestrantes (seis pessoas, com id, nome, instituicao, area, tema e foto) e o dicionário nomesDasAreas. O js/palestrantes.js de hoje consome os dois: ele não redeclara nem copia nada. Se você redeclarar aqui o array com const, as duas declarações colidem no escopo global e o navegador derruba o script inteiro com Uncaught SyntaxError: Identifier 'palestrantes' has already been declared.

Substitua o <main> de palestrantes.html pelo contêiner vazio, os botões de filtro e o contador — os seis <article> escritos à mão nas Aulas 02 e 04 saem daqui, porque a partir de agora quem os escreve é o JavaScript:

palestrantes.html (trecho: <head> e <main>)

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Conheça os palestrantes da Semana Acadêmica de Sistemas de Informação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/dados.js" defer></script>
  <script src="js/app.js" defer></script>
  <script src="js/palestrantes.js" defer></script>
</head>
HTML
<main id="conteudo" tabindex="-1" class="container">
  <h1>Palestrantes</h1>
  <p>Conheça quem vai compartilhar conhecimento nesta edição.</p>

  <div id="filtros-area" class="filtros" role="group" aria-label="Filtrar por área">
    <button type="button" class="ativo" data-area="todas" aria-pressed="true">Todas</button>
    <button type="button" data-area="web" aria-pressed="false">Desenvolvimento Web</button>
    <button type="button" data-area="dados" aria-pressed="false">Ciência de Dados</button>
    <button type="button" data-area="seguranca" aria-pressed="false">Segurança</button>
    <button type="button" data-area="ia" aria-pressed="false">Inteligência Artificial</button>
  </div>

  <p id="contador-palestrantes" class="contador" role="status"></p>

  <ul id="lista-palestrantes" class="cartoes"></ul>
</main>

Os quatro data-area são exatamente os códigos de nomesDasAreas (web, dados, seguranca, ia) — os mesmos que a Aula 12 usou nos relatórios do console. Repare também na ordem dos <script> no <head>: dados.js antes de palestrantes.js, senão o array ainda não existe quando o segundo arquivo roda. Com defer, a ordem de execução é a ordem em que os <script> aparecem.

site-evento/js/palestrantes.js

JavaScript
// palestrantes.js — a lista de palestrantes renderizada a partir de js/dados.js.
// Depende de js/dados.js (arrays `palestrantes` e objeto `nomesDasAreas`),
// que precisa ser carregado antes. Nada é redeclarado aqui.

// ===== ESTADO =====
let areaAtual = "todas";

// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====
const elementosPalestrantes = {
  lista: document.querySelector("#lista-palestrantes"),
  filtros: document.querySelector("#filtros-area"),
  contador: document.querySelector("#contador-palestrantes"),
};

// ===== FUNÇÕES DE DADOS =====
function obterPalestrantesVisiveis() {
  if (areaAtual === "todas") {
    return [...palestrantes];
  }
  return palestrantes.filter((pessoa) => pessoa.area === areaAtual);
}

// ===== RENDERIZAÇÃO =====
/**
 * Monta o cartão de um palestrante.
 * @param {Object} pessoa - um item do array `palestrantes` de dados.js
 * @returns {HTMLLIElement} o <li> pronto para entrar na lista
 */
function criarCartaoDePalestrante(pessoa) {
  const item = document.createElement("li");
  item.classList.add("cartao", "cartao--palestrante");
  item.dataset.id = pessoa.id;

  const foto = document.createElement("img");
  foto.src = pessoa.foto;
  foto.alt = `Foto de ${pessoa.nome}`;
  foto.width = 240;
  foto.height = 240;
  foto.loading = "lazy";

  const nome = document.createElement("h2");
  nome.textContent = pessoa.nome; // textContent: nunca innerHTML para dados

  const instituicao = document.createElement("p");
  instituicao.classList.add("cartao__meta");
  instituicao.textContent = pessoa.instituicao;

  const etiqueta = document.createElement("span");
  etiqueta.classList.add("etiqueta");
  etiqueta.textContent = nomesDasAreas[pessoa.area];

  const tema = document.createElement("p");
  tema.textContent = pessoa.tema;

  item.append(foto, nome, instituicao, etiqueta, tema);
  return item;
}

function renderizarPalestrantes() {
  const visiveis = obterPalestrantesVisiveis();
  elementosPalestrantes.lista.innerHTML = "";

  if (visiveis.length === 0) {
    const aviso = document.createElement("li");
    aviso.classList.add("vazio");
    aviso.textContent = "Nenhum palestrante nesta área ainda. Volte em breve!";
    elementosPalestrantes.lista.appendChild(aviso);
  } else {
    const fragmento = document.createDocumentFragment();
    visiveis.forEach((pessoa) => fragmento.appendChild(criarCartaoDePalestrante(pessoa)));
    elementosPalestrantes.lista.appendChild(fragmento);
  }

  const total = visiveis.length;
  elementosPalestrantes.contador.textContent =
    total === 1 ? "1 palestrante" : `${total} palestrantes`;
}

function atualizarBotoesDeFiltro() {
  elementosPalestrantes.filtros.querySelectorAll("button[data-area]").forEach((botaoFiltro) => {
    const ativo = botaoFiltro.dataset.area === areaAtual;
    botaoFiltro.classList.toggle("ativo", ativo);
    botaoFiltro.setAttribute("aria-pressed", String(ativo));
  });
}

// ===== EVENTOS =====
function registrarEventosDePalestrantes() {
  // Um único ouvinte para todos os botões de filtro (delegação, seção 14)
  elementosPalestrantes.filtros.addEventListener("click", (e) => {
    const botaoFiltro = e.target.closest("button[data-area]");
    if (!botaoFiltro) return;

    areaAtual = botaoFiltro.dataset.area;
    atualizarBotoesDeFiltro();
    renderizarPalestrantes();
  });
}

// ===== INICIALIZAÇÃO =====
function iniciarPalestrantes() {
  registrarEventosDePalestrantes();
  renderizarPalestrantes();
}

iniciarPalestrantes();

Duas escolhas de nome merecem explicação. Primeiro, elementosPalestrantes em vez de um genérico els: como todos os scripts sem type="module" dividem o mesmo escopo global, um nome curto e óbvio é justamente o que vai colidir com o de outro arquivo — na Aula 14 nasce um js/programacao.js que precisaria do mesmo els. Segundo, renderizarPalestrantes em vez de renderizar: pelo mesmo motivo. Nome específico é seguro; nome genérico é uma bomba-relógio.

Passo 4 — O estilo dos filtros e da etiqueta

Os cartões não precisam de CSS novo: <li class="cartao cartao--palestrante"> dentro de <ul class="cartoes"> reaproveita a grade repeat(auto-fit, minmax(280px, 1fr)) da Aula 07 e a foto redonda da Aula 08. Só os filtros, o contador, a etiqueta e o estado vazio são novos.

css/estilo.css (seção 5 — componentes)

CSS
/* ===== Filtros de área (Aula 13) ===== */
.filtros {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espaco-pequeno);
  margin-block: var(--espaco-medio);
}

.filtros button {
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border: 2px solid var(--cor-primaria);
  border-radius: 999px;
  background: transparent;
  color: var(--cor-primaria);
  font: inherit;
  cursor: pointer;
}

.filtros button.ativo {
  background: var(--cor-primaria);
  color: var(--cor-sobre-primaria);
}

.contador {
  color: var(--cor-texto);
  font-size: .875rem;
}

.etiqueta {
  justify-self: center;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--cor-fundo);
  border: 1px solid var(--cor-borda);
  font-size: .8rem;
}

.vazio {
  grid-column: 1 / -1;
  padding: var(--espaco-grande);
  text-align: center;
}

Nenhuma cor escrita à mão: todas saem do sistema de design da Aula 06 mais as variáveis que as Aulas 07 e 08 acrescentaram (--cor-superficie, --cor-borda, --cor-sobre-primaria). O :focus-visible global da Aula 06 já cuida do foco dos botões — não é preciso repeti-lo aqui.

Como testar

  1. Abra index.html com o Live Server e estreite a janela para menos de 768 px (ou use o modo dispositivo do DevTools, Ctrl+Shift+M). O botão "Menu" aparece; a lista, não.
  2. Clique no botão: a lista desliza e aparece com a animação da Aula 09 e, na aba Elements, aria-expanded muda para "true". O ícone gira 90°.
  3. Pressione Esc: o menu fecha e o foco volta ao botão (o anel de foco aparece nele). Abra de novo e clique fora do menu: fecha.
  4. Alargue a janela para além de 768 px: o menu fecha sozinho e o botão desaparece.
  5. Ainda no index.html, confira que a contagem regressiva continua na tela e que o Console ainda mostra a saudação, o grupo "Dados do evento" e a tabela das cinco páginas — se algo sumiu, o app.js foi sobrescrito em vez de ampliado.
  6. Em qualquer página, na aba Elements, o link do menu correspondente à página aberta tem aria-current="page"; os outros quatro, não. Navegue para outra página e confira que o atributo mudou de lugar.
  7. Vá a palestrantes.html: os seis cartões aparecem gerados pelo JavaScript (em Elements o <ul> está cheio; em "Exibir código-fonte", vazio). O contador mostra "6 palestrantes" e as fotos são as img/palestrante-01.jpg a img/palestrante-06.jpg.
  8. Clique em "Segurança": só a Carla Mendes aparece e o contador diz "1 palestrante". Clique em "Desenvolvimento Web": aparecem Diego Nascimento e Eduarda Ribeiro. Clique em "Todas": tudo volta. Navegue pelos filtros com Tab e acione com Enter — funciona igual.
  9. Console limpo nas cinco páginas: nenhuma linha vermelha e, em especial, nenhum Identifier 'palestrantes' has already been declared nem Identifier 'menu' has already been declared.

🧪 Laboratório

Nível A — Fixação

A1. Escreva a mesma função nas três formas (declaração, expressão e arrow): recebe o raio e retorna a área do círculo (Math.PI * raio ** 2).

A2. O que faz o operador rest (...numeros) em um parâmetro de função? E o spread em uma chamada? Dê um exemplo de cada.

A3. O que é o DOM? Ele é o mesmo que o arquivo HTML? Justifique com o que você vê em "Exibir código-fonte" versus a aba Elements.

A4. Qual a diferença entre querySelector e querySelectorAll? O que cada um retorna quando não encontra nada?

A5. Qual a diferença entre textContent, innerText e innerHTML?

A6. Explique o que é XSS e como textContent previne esse ataque.

A7. Escreva o código para: (a) selecionar o elemento de id menu; (b) selecionar todos os <li> dentro de .nav; (c) selecionar o primeiro input do tipo email.

A8. Escreva o código que adiciona a classe ativo, remove inativo e alterna aberto em um elemento.

A9. Escreva os três passos para criar e inserir um novo <li> com texto "Item" em uma <ul> existente.

A10. O que faz closest(".card")? Dê um caso de uso.

A11. Por que é preferível trocar classes a definir element.style diretamente?

A12. Um querySelector retornou null e o código quebrou com Cannot read properties of null. Cite duas causas prováveis e como corrigir cada uma.

A13. Por que addEventListener é preferível a elemento.onclick?

A14. Diferencie event.target de event.currentTarget com um exemplo concreto.

A15. Descreva as três fases da propagação de eventos.

A16. O que faz event.preventDefault()? Dê dois casos de uso.

A17. Qual a diferença entre stopPropagation() e preventDefault()?

A18. O que é delegação de eventos e qual problema ela resolve?

A19. Diferencie os eventos input e change. Quando usar cada um?

A20. Escreva o código que executa uma função quando o usuário pressiona Enter em um campo de texto.

A21. Escreva o código que impede o recarregamento da página ao enviar um formulário.

A22. O que faz a opção { once: true } em addEventListener?

A23. Por que <div onclick=""> é pior que <button> do ponto de vista de acessibilidade? Cite três motivos.

Nível B — Aplicação

B1. Implemente calculadora como um objeto com os métodos somar, subtrair, multiplicar, dividir (tratando divisão por zero) e potencia, todos com validação de tipo dos parâmetros (typeof).

Resultado esperado: calculadora.dividir(10, 0) retorna null (ou lança um erro com mensagem clara); calculadora.somar("2", 3) rejeita a string; calculadora.potencia(2, 10) retorna 1024.

Dica

Crie uma função auxiliar saoNumeros(...valores) que use valores.every((v) => typeof v === "number" && !Number.isNaN(v)) e chame-a no início de cada método. Métodos de objeto precisam de this? Aqui não — cada método só usa seus parâmetros, então arrow functions ou métodos curtos servem.

B2. Dada uma lista <ul> com 10 <li>, escreva o JavaScript que: pinta os ímpares de cinza-claro (via classe); adiciona a classe destaque ao primeiro e ao último; adiciona um número de ordem antes do texto de cada um; e exibe no console o texto de todos.

Resultado esperado: a lista alterna cores, o primeiro e o último ficam em negrito, cada item começa com "1. ", "2. " e assim por diante, e o console mostra os 10 textos.

Dica

querySelectorAll("ul li").forEach((li, indice) => …) — o segundo parâmetro do forEach é o índice. Para o texto, li.textContent = `${indice + 1}. ${li.textContent}`. Para o primeiro e o último, compare indice com 0 e com lista.length - 1.

B3. Construa uma tabela dinâmica: dado um array de alunos com três notas, gere uma <table> completa com <thead>, <tbody>, média calculada e uma coluna "Situação" cuja célula recebe a classe .aprovado, .exame ou .reprovado conforme o valor (≥ 6, ≥ 4, abaixo).

Resultado esperado: uma tabela com uma linha por aluno, a média com duas casas (toFixed(2)) e a célula de situação colorida pelo CSS.

Dica

Crie criarLinha(aluno) que devolve um <tr> com <td> gerados por createElement. Use reduce para a média e a função classificar da seção 1 para a situação. Monte tudo em um DocumentFragment e insira no <tbody> uma vez.

B4. Escreva uma função criarElemento(tag, atributos, filhos) que crie e configure um elemento genericamente. Exemplo de uso:

JavaScript
criarElemento("a", { href: "#", class: "btn", "data-id": 5 }, ["Clique aqui"]);

Use-a para reconstruir a renderização de produtos da seção 9 em menos linhas.

Resultado esperado: criarElemento("li", { class: "item" }, ["Texto", criarElemento("strong", {}, ["!"])]) gera <li class="item">Texto<strong>!</strong></li>.

Dica

Object.entries(atributos).forEach(([nome, valor]) => el.setAttribute(nome, valor)). Para os filhos, se for string use document.createTextNode; se for elemento, appendChild direto — el.append(...filhos) já aceita os dois tipos.

B5. Implemente um modal: botão que abre, botão "×" que fecha, clique no fundo escurecido que fecha, tecla Escape que fecha. Ao abrir, o foco deve ir para dentro do modal; ao fechar, voltar ao botão que o abriu.

Resultado esperado: o modal funciona só com o teclado (Tab até o botão, Enter abre, Esc fecha) e o foco nunca "some".

Dica

Use o elemento nativo <dialog> com dialog.showModal() e dialog.close() — ele já trata Escape e o foco inicial. Para o clique no fundo, ouça click no próprio dialog e feche se e.target === dialog. Guarde document.activeElement antes de abrir para devolver o foco ao fechar.

B6. Crie um menu suspenso (dropdown) que abre ao clicar, fecha ao clicar fora (use delegação em document), fecha com Escape e navega com as setas do teclado.

Resultado esperado: move o foco para o próximo item, para o anterior, Esc fecha e devolve o foco ao botão.

Dica

Guarde os itens em [...menu.querySelectorAll("a")] e o índice do item focado. No keydown, calcule o novo índice com (indice + 1) % itens.length (e (indice - 1 + itens.length) % itens.length para subir) e chame .focus(). O padrão de "clique fora" é o mesmo do Mão na massa.

B7. Implemente um contador de caracteres em um <textarea> com limite de 280: exibe os restantes, fica amarelo abaixo de 40 e vermelho abaixo de 10, e desabilita o botão de envio ao ultrapassar.

Resultado esperado: "280 caracteres restantes" ao carregar; ao digitar, o número cai; com 281 caracteres o botão fica disabled e o texto, vermelho.

Dica

Ouça input no textarea, calcule restantes = LIMITE - textarea.value.length e use classList.toggle("alerta", restantes < 40) e classList.toggle("perigo", restantes < 10). botao.disabled = restantes < 0.

B8. Crie uma lista de 20 itens gerados dinamicamente onde cada item tem botões "subir", "descer" e "excluir", todos tratados por um único ouvinte no contêiner (delegação).

Resultado esperado: clicar em "subir" troca o item de posição com o anterior; "excluir" remove; a lista continua funcionando depois de qualquer operação porque é re-renderizada a partir do array.

Dica

Não mova o <li> no DOM — troque as posições no array com [itens[i - 1], itens[i]] = [itens[i], itens[i - 1]] e chame renderizar(). Use data-acao e data-id nos botões e o switch da seção 14.

Nível C — Desafio

C1. Kanban com arrastar e soltar. Construa um quadro com três colunas (A fazer, Em andamento, Concluído). Cartões podem ser criados, editados, excluídos e arrastados entre colunas usando a API de Drag and Drop (dragstart, dragover, drop). Contadores por coluna. Deve funcionar também por teclado (botões "mover para →" e "← mover para").

Dica

Cada cartão tem draggable="true" e data-id. No dragstart, guarde o id em e.dataTransfer.setData("text/plain", id). A coluna precisa chamar e.preventDefault() no dragover para aceitar o drop. No drop, leia o id, mude o campo coluna do cartão no array e renderize tudo — o mesmo fluxo estado → renderização de sempre. Os botões de teclado fazem exatamente a mesma alteração no array.

🏆 Desafios

⭐ O botão que só funciona uma vez

javascripteventosbugdevtools

Alguém enviou o código abaixo dizendo que "o contador funciona uma vez e depois para, e às vezes nem começa". Rode-o, reproduza os dois sintomas e encontre os quatro bugs plantados — sem reescrever do zero. Cada um é um erro clássico visto nesta aula.

HTML
<!-- bug-contador.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Contador</title>
  <script src="contador.js"></script>
</head>
<body>
  <p>Cliques: <strong id="total">0</strong></p>
  <button id="btn-somar"><span>Somar</span></button>
  <button id="btn-zerar">Zerar</button>
</body>
</html>
JavaScript
// contador.js
let total = 0;
const saida = document.querySelector("#total");
const btnSomar = document.querySelector("#btn-somar");
const btnZerar = document.querySelector("#btn-zerar");

function somar(e) {
  if (e.target !== btnSomar) return;
  total = total + 1;
  saida.textContent = total;
}

btnSomar.addEventListener("click", somar, { once: true });
btnZerar.addEventListener("click", zerar());

function zerar() {
  total = 0;
  saida.textContent = total;
}

Critérios de pronto

  • O contador incrementa a cada clique, indefinidamente, inclusive quando o clique cai no texto "Somar".
  • "Zerar" volta o contador a 0 apenas quando clicado.
  • Nenhum erro no console ao carregar a página.
  • Um comentário acima de cada correção diz qual era o bug e por que acontecia (uma frase cada).
Pistas
  1. Abra o Console antes de qualquer coisa: uma das falhas aparece em vermelho no carregamento e tem a ver com a ordem em que o navegador lê <head> e <body>.
  2. Releia a seção 12 sobre target × currentTarget e pense no <span> dentro do botão.
  3. Uma opção passada ao addEventListener está fazendo exatamente o que promete.
  4. Compare zerar com zerar() — o que está sendo passado como ouvinte?
⭐⭐

⭐⭐ Lista de tarefas interativa

javascriptdomeventosprojeto

Uma lista de tarefas é o "olá, mundo" das interfaces dinâmicas — e o exercício que mais revela se você entendeu o fluxo estado → renderização → eventos. Construa a sua, do zero, seguindo a arquitetura da seção 18, sem alterar o DOM "na mão" em nenhum momento: toda mudança na tela nasce de uma mudança no array seguida de renderizar().

Critérios de pronto

  • Adicionar pelo botão ou pela tecla Enter; texto vazio ou só com espaços é rejeitado com mensagem de erro visível.
  • Concluir: clicar no checkbox alterna concluida no array e re-renderiza.
  • Excluir: botão com data-id, tratado por delegação, com confirmação.
  • Editar: duplo clique no texto o transforma em <input>; Enter confirma, Escape cancela, blur salva.
  • Filtrar: três botões (Todas / Pendentes / Concluídas) alteram uma variável de estado e re-renderizam; o botão ativo recebe a classe .ativo e aria-pressed="true".
  • Buscar: campo de texto com input + debounce de 300 ms, filtrando pelo termo.
  • Ordenar: <select> com as opções mais recentes, mais antigas e alfabética.
  • Limpar concluídas remove todas de uma vez, com confirmação.
  • Contadores de total, pendentes e concluídas, sempre atualizados.
  • Atalhos: Ctrl+Enter adiciona; Escape limpa o campo.
  • O arquivo js/tarefas.js está dividido nos blocos ESTADO, ELEMENTOS, DADOS, RENDERIZAÇÃO, EVENTOS e INICIALIZAÇÃO, nessa ordem.
Pistas
  1. Comece pelo estado: let tarefas = [], let filtroAtual = "todas", let termoBusca = "", let ordenacao = "recentes". Cada tarefa é { id, texto, concluida, criadaEm } com id: Date.now().
  2. Escreva obterTarefasVisiveis() como uma cadeia: copia o array, aplica filter do filtro, filter da busca e sort da ordenação — e só ela é usada por renderizar().
  3. Para editar, guarde idEmEdicao no estado; renderizar() desenha um <input> em vez do texto quando o id bate. Enter/Escape/blur apenas alteram o estado e renderizam.
  4. Um único addEventListener("click") na <ul> resolve checkbox, excluir e iniciar edição — use closest("[data-acao]") e um switch.
⭐⭐⭐

⭐⭐⭐ Quiz com pontuação e cronômetro

javascriptdomeventosacessibilidade

Sites de evento costumam ter um "quiz de conhecimentos" para engajar quem se inscreve. Construa um quiz de 10 perguntas de múltipla escolha sobre a Unidade 3 (você escreve as perguntas), com uma pergunta por tela, cronômetro de 20 segundos por questão, pontuação e um placar final. As perguntas vivem em um array de objetos; a tela é renderizada a partir do estado; não existe uma linha de HTML de pergunta escrita à mão.

Critérios de pronto

  • Uma pergunta por vez, com quatro alternativas em <button>; responder ou esgotar o tempo avança automaticamente.
  • Barra de tempo animada com CSS e atualizada por setInterval, que respeita prefers-reduced-motion.
  • Pontuação: 10 pontos por acerto mais um bônus de 1 ponto por segundo restante; a fórmula está comentada no código.
  • Navegação completa por teclado: teclas 14 selecionam a alternativa; Enter confirma.
  • Ao terminar, um placar mostra acertos, erros, tempo médio e as questões erradas com a resposta certa.
  • O quiz dispara um CustomEvent("quizFinalizado", { detail }) com o resultado, e um script separado escuta esse evento para gravar o melhor placar em localStorage.
  • Nenhum innerHTML recebe texto vindo das perguntas — tudo por textContent.
Pistas
  1. Estado mínimo: indiceAtual, pontuacao, respostas (array), segundosRestantes e o id do setInterval — para poder cancelar com clearInterval ao responder.
  2. Escreva renderizarPergunta() e renderizarPlacar() como funções separadas; avancar() decide qual chamar.
  3. Para as teclas 1–4, ouça keydown em document e use Number(e.key) - 1 como índice da alternativa — mas ignore quando o quiz não estiver em andamento.
  4. localStorage.setItem("quiz:melhor", JSON.stringify(detail)) no ouvinte do evento personalizado; leia com JSON.parse ao carregar. A próxima aula aprofunda o localStorage.

🐛 Erros comuns

Sintoma Causa Solução
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') querySelector não encontrou o elemento: seletor errado ou script no <head> sem defer Confira o seletor no Console com $$("seletor"); coloque defer na tag <script>
Clicar no botão não faz nada e não há erro addEventListener("click", fn()) — a função foi chamada no registro e o retorno (undefined) virou o ouvinte Passe a referência fn, sem parênteses
Página recarrega ao enviar o formulário e "os dados somem" Falta e.preventDefault() no ouvinte de submit Chame e.preventDefault() na primeira linha do ouvinte
Ouvinte registrado no botão, mas Enter no campo envia o formulário sem passar por ele O evento certo é submit no <form>, não click no botão Ouça submit no formulário
Itens criados pelo JavaScript não reagem ao clique Ouvintes foram registrados antes de os elementos existirem Use delegação: um ouvinte no contêiner + closest()
e.target.dataset.id é undefined em alguns cliques O clique caiu em um filho do botão (ícone, <span>) — target é o filho Use e.target.closest("button[data-id]")
if (botao.dataset.id === produto.id) nunca é verdadeiro dataset sempre devolve string; produto.id é número Converta com Number(botao.dataset.id)
removeEventListener não remove nada Foi passada uma função anônima diferente da registrada Nomeie a função e passe a mesma referência nos dois lugares
Busca ao digitar trava a página em listas grandes Um filtro pesado roda a cada tecla Envolva o ouvinte em debounce(fn, 300)
<div onclick> não funciona com Tab/Enter nem é lido pelo leitor de tela div não é focável nem tem semântica de controle Troque por <button type="button">
Menu abre e fecha no mesmo clique O clique no botão borbulha até o ouvinte de "clique fora" em document Verifique botao.contains(e.target) antes de fechar (ou não use stopPropagation)

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulos de funções, de scripting de documentos e de eventos. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, capítulo de interação com o usuário. Na MDN em português, o guia "Introdução a eventos" (link em Para aprofundar).

Parte 2 — Entrega (40 min). No seu projeto autoral:

  1. O menu mobile funcionando com clique, Esc e clique fora, com aria-expanded sincronizado (como no Mão na massa).
  2. A listagem principal do seu domínio (produtos, quadras, vagas, pratos, pescarias) renderizada a partir de um array de pelo menos 6 objetos, com estado vazio tratado.
  3. Ao menos um filtro por botões usando delegação de eventos.
  4. Os exercícios B7 (contador de caracteres, aplicado ao seu formulário de contato) e B8 (lista com subir/descer/excluir) em uma pasta exercicios/aula13/.

Critério de pronto: nenhuma página do projeto mostra erro no Console; a listagem some do "Exibir código-fonte" e aparece na aba Elements; o menu abre e fecha só com o teclado.

Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: explique, com um trecho do seu projeto, um problema que só a delegação de eventos resolve bem — e o que aconteceria sem ela.

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

📚 Para aprofundar

Na próxima aula, o formulário de inscrição do evento ganha validação campo a campo com mensagens acessíveis, expressões regulares para CPF, telefone e e-mail, e a programação passa a ter busca, filtro e ordenação em tempo real — tudo construído sobre as funções, o DOM e os eventos de hoje.

WebLab — Laboratório de Desenvolvimento Web
Conteúdo sob CC BY 4.0; o gerador do site, sob licença MIT. Reuso livre com atribuição.
Início · Autoria e créditos · Banco de Desafios · Links úteis · Fontes no GitHub