[ ] VS Code com a extensão Live Server funcionando e o DevTools do navegador aberto na aba Console.
[ ] O site do evento acadêmico com as cinco páginas estilizadas e responsivas (Unidade 2) e o js/app.js ligado com defer.
[ ] js/dados.js e js/relatorios.js (Aula 12) no projeto, com os arrays palestrantes e palestras e o objeto nomesDasAreas — hoje eles saem do console e vão para a tela.
[ ] Conforto com arrays e objetos: forEach, map, filter, find e a notação { chave: valor } (Aula 12).
[ ] Seletores CSS (Aula 06): #id, .classe, elemento, [atributo], descendente e :checked — querySelector usa exatamente essa sintaxe.
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.
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çõ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.
constcalcularMedia=(a1,a2,a3)=>(a1+a2+a3)/3;// Um parâmetro: parênteses opcionaisconstdobrar=(n)=>n*2;// Sem parâmetrosconstsaudacao=()=>"Olá!";// Corpo com várias linhas exige chaves e return explícitoconstclassificar=(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çãoconstcriarAluno=(nome,nota)=>({nome,nota});console.log(dobrar(21));// 42console.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
// Valor padrão: usado quando o argumento não é passado (ou é undefined)functionsaudar(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 arrayfunctionsomar(...numeros){returnnumeros.reduce((acumulado,n)=>acumulado+n,0);}somar(1,2,3,4,5);// 15// Spread (...) na chamada: "espalha" um array como argumentos separadosconstnotas=[8,7,9];calcularMedia(...notas);// o mesmo que calcularMedia(8, 7, 9)// Objeto como parâmetro: argumentos nomeados, em qualquer ordemfunctioncadastrar({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.
constglobal="visível em toda parte";functionexemplo(){constlocal="só dentro desta função";if(true){letbloco="só dentro deste bloco";consttambemBloco="idem";varvazado="vaza para a função inteira";// por isso não use var}console.log(bloco);// ReferenceError: bloco is not definedconsole.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 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:
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.
Uma função, uma responsabilidade. Se o nome tem "e" (salvarEEnviar), provavelmente são duas funções.
Nomes são verbos:calcularMedia, validarEmail, formatarData, renderizarLista.
Evite mais de 3–4 parâmetros. Acima disso, receba um objeto.
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.
Retorne cedo para reduzir aninhamento:
JavaScript
// Em vez de aninhar três ifsfunctionprocessar(dado){if(!dado)returnnull;if(!dado.ativo)returnnull;returndado.valor*2;}
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.
document
└── html
└── body
├── h1#titulo
│ └── "Olá" (nó de texto)
└── ul.lista
├── li → "Item 1"
└── li → "Item 2"
Três verdades importantes:
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.
Alterar o DOM altera a tela imediatamente. Não há "salvar" nem "atualizar".
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.
// Retorna o PRIMEIRO elemento que casa com o seletor CSS (ou null)consttitulo=document.querySelector("#titulo");constprimeiroItem=document.querySelector(".lista li");constbotao=document.querySelector("button.primario");constcampo=document.querySelector("input[type='email']");// Retorna TODOS os que casam, como NodeList (mesmo que vazia)constitens=document.querySelectorAll(".lista li");constparagrafos=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.
document.getElementById("titulo");// sem #document.getElementsByClassName("lista");// HTMLCollection AO VIVOdocument.getElementsByTagName("li");// HTMLCollection AO VIVOdocument.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.
constitens=document.querySelectorAll("li");itens.forEach((item)=>console.log(item.textContent));// Se precisar de métodos de array (map, filter, find), converta:consttextos=Array.from(itens).map((i)=>i.textContent);consttextos2=[...itens].map((i)=>i.textContent);
⚠️ AtençãoquerySelector 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").
consttitulo=document.querySelector("#titulo");// LEITURAtitulo.textContent;// texto puro, incluindo o de elementos ocultostitulo.innerText;// texto como renderizado (respeita CSS, mais lento)titulo.innerHTML;// conteúdo HTML como string// ESCRITAtitulo.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.
constlink=document.querySelector("a");link.getAttribute("href");link.setAttribute("href","https://www.wikipedia.org");link.setAttribute("target","_blank");link.hasAttribute("download");// true/falselink.removeAttribute("target");// Propriedades diretas para atributos comunslink.href;link.id;img.src;img.alt;input.value;// valor atual do campoinput.checked;// true/false para checkbox/radioinput.disabled=true;select.value;
💡 Dicainput.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.
constbtn=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.
constcard=document.querySelector(".card");card.classList.add("ativo");card.classList.add("destaque","novo");// várias de uma vezcard.classList.remove("ativo");card.classList.toggle("aberto");// adiciona se não tem, remove se temcard.classList.toggle("aberto",condicao);// força adicionar (true) ou remover (false)card.classList.contains("ativo");// true/falsecard.classList.replace("antigo","novo");
⚠️ Atenção
Evite card.className = "novo" — isso substitui todas as classes existentes de uma vez.
card.style.backgroundColor="#0b3d5c";// camelCase, não background-colorcard.style.fontSize="18px";// sempre com unidadecard.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)constestilo=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 JavaScriptel.style.display="none";el.style.opacity="0";// O CSS define o que é "oculto"; o JS só decide quandoel.classList.add("oculto");
// 1. Criarconstli=document.createElement("li");// 2. Configurarli.textContent="Novo item";li.classList.add("item");li.dataset.id="7";// 3. Inserirconstlista=document.querySelector("ul");lista.appendChild(li);// no fimlista.append(li,outroLi);// no fim, aceita vários nós e textolista.prepend(li);// no iníciolista.insertBefore(li,referencia);// antes de um filho específico// Inserção posicional em relação a um elementoelemento.insertAdjacentElement("beforebegin",novo);// antes do elementoelemento.insertAdjacentElement("afterbegin",novo);// primeiro filhoelemento.insertAdjacentElement("beforeend",novo);// último filhoelemento.insertAdjacentElement("afterend",novo);// depois do elementoelemento.insertAdjacentHTML("beforeend","<li>Item</li>");// com string HTML
elemento.remove();// remove a si mesmopai.removeChild(filho);// forma antigapai.replaceChild(novo,antigo);elemento.replaceWith(novo);lista.innerHTML="";// esvazia (rápido e comum)
constitem=document.querySelector(".item");// Para cimaitem.parentElement;item.closest(".card");// ancestral mais próximo que casa com o seletor (inclui o próprio)// Para baixoitem.children;// HTMLCollection de elementos filhositem.firstElementChild;item.lastElementChild;item.querySelector(".titulo");// busca DENTRO do item// Para os ladositem.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.
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.jsconstprodutos=[{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"},];constcontainer=document.querySelector("#lista-produtos");functionrenderizar(lista){container.innerHTML="";// 1. limpaif(lista.length===0){// 2. estado vazioconstaviso=document.createElement("p");aviso.classList.add("vazio");aviso.textContent="Nenhum produto encontrado.";container.appendChild(aviso);return;}lista.forEach((produto)=>{// 3. cria um nó por itemconstcard=document.createElement("article");card.classList.add("card-produto");card.dataset.id=produto.id;consttitulo=document.createElement("h3");titulo.textContent=produto.nome;// textContent: seguro contra XSSconstpreco=document.createElement("p");preco.classList.add("preco");preco.textContent=produto.preco.toLocaleString("pt-BR",{style:"currency",currency:"BRL",});constbtn=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);
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.
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
constfragmento=document.createDocumentFragment();lista.forEach((item)=>{constli=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.
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.
constbotao=document.querySelector("#meu-botao");botao.addEventListener("click",function(evento){console.log("Clicado!",evento.target);});// Com arrow functionbotao.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");});
<!-- 1. Atributo HTML — evite: mistura comportamento com estrutura --><buttononclick="alert('Oi')">Clique</button>
JavaScript
// 2. Propriedade do elemento — permite apenas UM ouvinte por eventobotao.onclick=function(){console.log("A");};botao.onclick=function(){console.log("B");};// sobrescreve o anterior!// 3. addEventListener — a forma corretabotao.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.
elemento.addEventListener(tipo,funcao,opcoes);// Exemplosbotao.addEventListener("click",tratarClique);botao.addEventListener("click",()=>console.log("Oi"));botao.addEventListener("click",tratarClique,{once:true,// executa apenas uma vez e se removecapture: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çãofunctiontratarClique(){console.log("Oi");}botao.addEventListener("click",tratarClique);botao.removeEventListener("click",tratarClique);// funcionabotao.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 todosbotao.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).
campo.addEventListener("keydown",(e)=>{console.log(e.key);// "a", "Enter", "Escape", "ArrowUp", " "console.log(e.code);// "KeyA", "Enter", "Space" — posição física da teclaconsole.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 navegadorsalvar();}});
💡 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").
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.
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 eventoevent.currentTarget;// elemento onde o OUVINTE está registradoevent.timeStamp;// milissegundos desde o carregamento da páginaevent.clientX,event.clientY;// coordenadas na janelaevent.pageX,event.pageY;// coordenadas na página (com rolagem)event.preventDefault();// cancela o comportamento padrãoevent.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.
// Impede o envio e o recarregamento da páginaformulario.addEventListener("submit",(e)=>{e.preventDefault();processarDados();});// Impede a navegação de um linklink.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.
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 subindoevent.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.
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
constlista=document.querySelector("#lista-produtos");lista.addEventListener("click",(event)=>{// Encontra o botão, mesmo se o clique foi num ícone dentro deleconstbotaoExcluir=event.target.closest(".btn-excluir");if(!botaoExcluir)return;// clique fora dos botões: ignoraconstid=Number(botaoExcluir.dataset.id);excluirProduto(id);});
Vantagens:
Funciona para elementos criados depois — sem re-registrar nada.
Um ouvinte em vez de N — menos memória, melhor desempenho.
Código mais simples — a lógica fica concentrada em um lugar.
Ú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.
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 msfunctiondebounce(fn,atraso=300){lettimer;returnfunction(...args){clearTimeout(timer);timer=setTimeout(()=>fn.apply(this,args),atraso);};}constbuscar=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 msfunctionthrottle(fn,intervalo=200){letpodeExecutar=true;returnfunction(...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.
// Só funciona com mousediv.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 telabotao.addEventListener("click",acao);
Se por algum motivo precisar tornar um elemento genérico interativo:
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 =====lettarefas=[];letfiltroAtual="todas";// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====constels={form:document.querySelector("#form-tarefa"),input:document.querySelector("#input-tarefa"),lista:document.querySelector("#lista-tarefas"),};// ===== FUNÇÕES DE DADOS (puras: alteram só o estado) =====functionadicionarTarefa(texto){tarefas.push({id:Date.now(),texto,concluida:false});}functionalternarConclusao(id){consttarefa=tarefas.find((t)=>t.id===id);if(tarefa)tarefa.concluida=!tarefa.concluida;}functionobterTarefasVisiveis(){if(filtroAtual==="pendentes")returntarefas.filter((t)=>!t.concluida);if(filtroAtual==="concluidas")returntarefas.filter((t)=>t.concluida);return[...tarefas];}// ===== RENDERIZAÇÃO (desenha a tela a partir do estado) =====functionrenderizar(){els.lista.innerHTML="";obterTarefasVisiveis().forEach((t)=>{constli=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) =====functionregistrarEventos(){els.form.addEventListener("submit",(e)=>{e.preventDefault();consttexto=els.input.value.trim();if(texto==="")return;adicionarTarefa(texto);els.input.value="";renderizar();});els.lista.addEventListener("click",(e)=>{constli=e.target.closest("li[data-id]");if(!li)return;alternarConclusao(Number(li.dataset.id));renderizar();});}// ===== INICIALIZAÇÃO =====functioniniciar(){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.
// Seleciona o botão e a lista pelo seletor CSSconstbotao=document.querySelector('.menu-botao');constmenu=document.querySelector('#menu-principal');// A cada clique, inverte o valor de aria-expandedbotao.addEventListener('click',()=>{constaberto=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 =====constbotao=document.querySelector(".menu-botao");constmenu=document.querySelector("#menu-principal");constLARGURA_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.functionmenuEstaAberto(){returnbotao.getAttribute("aria-expanded")==="true";}functiondefinirMenu(aberto){botao.setAttribute("aria-expanded",String(aberto));}functionfecharMenu(){definirMenu(false);}functionalternarMenu(){definirMenu(!menuEstaAberto());}// ===== EVENTOS =====functionregistrarEventosDoMenu(){// 1. Clique no botão: abre ou fechabotao.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)=>{constclicouDentro=menu.contains(e.target)||botao.contains(e.target);if(!clicouDentro&&menuEstaAberto()){fecharMenu();}});// 4. Ao alargar a janela para desktop, garante o estado fechadowindow.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:
O estado continua no aria-expanded. Não existe classe .aberto neste projeto: o CSS das Aulas 08 e 09 já reage ao atributo, e o atributo é o que o leitor de tela anuncia. Um estado, um lugar.
O clique fora usa o document — é delegação (seção 14) aplicada à página inteira. Repare que ele funciona porque o clique no botão também chega ao document na fase de bolha; por isso a verificação botao.contains(e.target), senão o menu abriria e fecharia no mesmo clique.
resize dispara dezenas de vezes por segundo ao arrastar a janela. Aqui o trabalho é baratíssimo (uma comparação e, no máximo, um setAttribute), então não vale a pena um throttle. Na Aula 14 você vai encontrar um caso em que vale.
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 =====constNOME_EVENTO="Semana Acadêmica de Sistemas de Informação";constEDICAO=12;constLOCAL_EVENTO="Auditório Central";constTRILHAS="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 "/" */functionmarcarPaginaAtual(padrao="index.html"){constlistaDoMenu=document.querySelector("#menu-principal");if(listaDoMenu===null)return;constarquivoAtual=location.pathname.split("/").pop()||padrao;listaDoMenu.querySelectorAll("a").forEach((link)=>{constdestino=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><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Conheça os palestrantes da Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title><linkrel="stylesheet"href="css/estilo.css"><scriptsrc="js/menu.js"defer></script><scriptsrc="js/dados.js"defer></script><scriptsrc="js/app.js"defer></script><scriptsrc="js/palestrantes.js"defer></script></head>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Palestrantes</h1><p>Conheça quem vai compartilhar conhecimento nesta edição.</p><divid="filtros-area"class="filtros"role="group"aria-label="Filtrar por área"><buttontype="button"class="ativo"data-area="todas"aria-pressed="true">Todas</button><buttontype="button"data-area="web"aria-pressed="false">Desenvolvimento Web</button><buttontype="button"data-area="dados"aria-pressed="false">Ciência de Dados</button><buttontype="button"data-area="seguranca"aria-pressed="false">Segurança</button><buttontype="button"data-area="ia"aria-pressed="false">Inteligência Artificial</button></div><pid="contador-palestrantes"class="contador"role="status"></p><ulid="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.jsantes 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 =====letareaAtual="todas";// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====constelementosPalestrantes={lista:document.querySelector("#lista-palestrantes"),filtros:document.querySelector("#filtros-area"),contador:document.querySelector("#contador-palestrantes"),};// ===== FUNÇÕES DE DADOS =====functionobterPalestrantesVisiveis(){if(areaAtual==="todas"){return[...palestrantes];}returnpalestrantes.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 */functioncriarCartaoDePalestrante(pessoa){constitem=document.createElement("li");item.classList.add("cartao","cartao--palestrante");item.dataset.id=pessoa.id;constfoto=document.createElement("img");foto.src=pessoa.foto;foto.alt=`Foto de ${pessoa.nome}`;foto.width=240;foto.height=240;foto.loading="lazy";constnome=document.createElement("h2");nome.textContent=pessoa.nome;// textContent: nunca innerHTML para dadosconstinstituicao=document.createElement("p");instituicao.classList.add("cartao__meta");instituicao.textContent=pessoa.instituicao;constetiqueta=document.createElement("span");etiqueta.classList.add("etiqueta");etiqueta.textContent=nomesDasAreas[pessoa.area];consttema=document.createElement("p");tema.textContent=pessoa.tema;item.append(foto,nome,instituicao,etiqueta,tema);returnitem;}functionrenderizarPalestrantes(){constvisiveis=obterPalestrantesVisiveis();elementosPalestrantes.lista.innerHTML="";if(visiveis.length===0){constaviso=document.createElement("li");aviso.classList.add("vazio");aviso.textContent="Nenhum palestrante nesta área ainda. Volte em breve!";elementosPalestrantes.lista.appendChild(aviso);}else{constfragmento=document.createDocumentFragment();visiveis.forEach((pessoa)=>fragmento.appendChild(criarCartaoDePalestrante(pessoa)));elementosPalestrantes.lista.appendChild(fragmento);}consttotal=visiveis.length;elementosPalestrantes.contador.textContent=total===1?"1 palestrante":`${total} palestrantes`;}functionatualizarBotoesDeFiltro(){elementosPalestrantes.filtros.querySelectorAll("button[data-area]").forEach((botaoFiltro)=>{constativo=botaoFiltro.dataset.area===areaAtual;botaoFiltro.classList.toggle("ativo",ativo);botaoFiltro.setAttribute("aria-pressed",String(ativo));});}// ===== EVENTOS =====functionregistrarEventosDePalestrantes(){// Um único ouvinte para todos os botões de filtro (delegação, seção 14)elementosPalestrantes.filtros.addEventListener("click",(e)=>{constbotaoFiltro=e.target.closest("button[data-area]");if(!botaoFiltro)return;areaAtual=botaoFiltro.dataset.area;atualizarBotoesDeFiltro();renderizarPalestrantes();});}// ===== INICIALIZAÇÃO =====functioniniciarPalestrantes(){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.
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);}.filtrosbutton{padding:var(--espaco-pequeno)var(--espaco-medio);border:2pxsolidvar(--cor-primaria);border-radius:999px;background:transparent;color:var(--cor-primaria);font:inherit;cursor:pointer;}.filtrosbutton.ativo{background:var(--cor-primaria);color:var(--cor-sobre-primaria);}.contador{color:var(--cor-texto);font-size:.875rem;}.etiqueta{justify-self:center;padding:2px10px;border-radius:999px;background:var(--cor-fundo);border:1pxsolidvar(--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.
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.
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°.
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.
Alargue a janela para além de 768 px: o menu fecha sozinho e o botão desaparece.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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
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>.
Releia a seção 12 sobre target × currentTarget e pense no <span> dentro do botão.
Uma opção passada ao addEventListener está fazendo exatamente o que promete.
Compare zerar com zerar() — o que está sendo passado como ouvinte?
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
Comece pelo estado: let tarefas = [], let filtroAtual = "todas", let termoBusca = "", let ordenacao = "recentes". Cada tarefa é { id, texto, concluida, criadaEm } com id: Date.now().
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().
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.
Um único addEventListener("click") na <ul> resolve checkbox, excluir e iniciar edição — use closest("[data-acao]") e um switch.
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 1–4 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
Estado mínimo: indiceAtual, pontuacao, respostas (array), segundosRestantes e o id do setInterval — para poder cancelar com clearInterval ao responder.
Escreva renderizarPergunta() e renderizarPlacar() como funções separadas; avancar() decide qual chamar.
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.
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.
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:
O menu mobile funcionando com clique, Esc e clique fora, com aria-expanded sincronizado (como no Mão na massa).
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.
Ao menos um filtro por botões usando delegação de eventos.
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).
[ ] js/menu.js, js/dados.js e js/app.js carregados com defer em todas as páginas, nessa ordem, sem erro no Console — e nenhum identificador declarado em dois arquivos.
[ ] js/app.jsampliado, não reescrito: a saudação e o console.table da Aula 10 e a contagem regressiva da Aula 11 continuam funcionando.
[ ] Botão hambúrguer com aria-expanded e aria-controls; menu abre/fecha por clique, Esc, clique fora e resize, sem classe .aberto e sem display: none.
[ ] Link da página atual marcado com aria-current="page" pelo JavaScript, e o atributo removido do HTML das cinco páginas.
[ ] Listagem principal renderizada a partir de um array de objetos, com textContent (nunca innerHTML para dados) e estado vazio tratado.
[ ] Filtro por botões com um único ouvinte (delegação) e aria-pressed atualizado.
[ ] Nenhum var, nenhum onclick no HTML, nenhum <div> clicável.
[ ] Código organizado nos blocos ESTADO → ELEMENTOS → DADOS → RENDERIZAÇÃO → EVENTOS → INICIALIZAÇÃO.
WHATWG — DOM Living Standard: https://dom.spec.whatwg.org/#events — a especificação oficial das fases de propagação, para quem quer ver a fonte.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulos sobre funções, scripting de documentos e tratamento de eventos.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — padrões de callbacks e de organização de código em módulos.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — capítulo de interação com o usuário.
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.