Capturar os valores de um formulário pelas três vias (querySelector, form.elements e FormData) e converter cada tipo de campo corretamente.
Escolher o momento certo de validar (input, blur, submit) e implementar a estratégia "valide no blur, corrija no input".
Usar a Constraint Validation API (validity, checkValidity, setCustomValidity, novalidate) combinando os atributos do HTML com regras de negócio em JavaScript.
Escrever funções validadoras puras, incluindo a verificação dos dígitos verificadores do CPF.
Ler e escrever expressões regulares para os formatos brasileiros usuais, reconhecendo o que a regex consegue e o que ela não consegue validar.
Exibir mensagens de erro específicas e acessíveis com role="alert", aria-invalid, aria-describedby e aria-live, levando o foco ao primeiro campo inválido.
Implementar busca, filtro e ordenação em tempo real sobre uma listagem, com normalização de acentos, debounce e tratamento do estado vazio.
[ ] O site do evento acadêmico com as cinco páginas responsivas, js/app.js carregado com defer em todas elas e o menu hambúrguer funcionando (Aula 13).
[ ] js/dados.js com os arrays palestras e palestrantes criados na Aula 12.
[ ] Domínio de funções, querySelector, classList, dataset, createElement, addEventListener, preventDefault e delegação de eventos (Aula 13).
[ ] Domínio de filter, map, find, forEach e sort sobre arrays de objetos (Aula 12).
[ ] O formulário de inscricao.html com <label>, <fieldset>, <select> e validação nativa (Aulas 03 e 04).
[ ] DevTools aberto na aba Console e o Live Server rodando.
Na aula passada o JavaScript saiu do console e entrou na página: você escreveu funções, manipulou o DOM, registrou ouvintes e renderizou os palestrantes a partir de um array, com filtro por delegação. Hoje o mesmo conjunto de ferramentas resolve os dois problemas que todo site real tem: impedir que dados errados entrem e deixar o usuário encontrar o que procura. O formulário de inscrição ganha validação campo a campo com mensagens acessíveis, e a programação passa a ter busca, filtro e ordenação em tempo real.
Todo processamento de formulário em JavaScript começa no mesmo lugar: o evento submit do <form> — nunca o click do botão. O motivo você já viu na Aula 13: Enter em qualquer campo de texto envia o formulário sem passar pelo botão, e um ouvinte de click simplesmente não é acionado.
constform=document.querySelector("#form-inscricao");form.addEventListener("submit",(evento)=>{evento.preventDefault();// SEMPRE: sem isso a página recarrega e tudo someconsole.log("Formulário enviado sem recarregar a página");});
O atributo novalidate no <form>desliga a validação nativa do navegador. Fazemos isso quando queremos controlar totalmente as mensagens: as caixas amarelas do Chrome não são personalizáveis, mudam de texto entre navegadores, somem sozinhas depois de alguns segundos e não são lidas de forma confiável por leitores de tela. Mesmo desligando a exibição, mantemos os atributos required, type, minlength e pattern no HTML: eles continuam alimentando a Constraint Validation API (seção 3) e documentam as regras para quem lê o HTML.
⚠️ Atençãonovalidate desliga a exibição das mensagens nativas, não a validação. campo.validity.valueMissing continua funcionando. É por isso que a combinação "atributos no HTML + mensagens em JavaScript" é a arquitetura recomendada: você não reescreve regras que o navegador já sabe aplicar.
constform=document.querySelector("#form-inscricao");// 1. Elemento por elemento — explícito, bom quando você precisa do elemento em siconstnome=document.querySelector("#nome").value.trim();// 2. Pela coleção elements do formulário — usa o atributo nameconstemail=form.elements.email.value.trim();// 3. FormData — a forma mais prática quando você quer o objeto inteiroconstdados=Object.fromEntries(newFormData(form).entries());console.log(dados);// { nome: "Ana Souza", email: "ana@exemplo.br", cpf: "123.456.789-09" }
new FormData(form) monta um objeto iterável com todos os campos que têm name e não estão desabilitados. Object.fromEntries transforma esses pares chave/valor em um objeto JavaScript comum. Duas limitações que valem a pena guardar: FormData ignora campos sem name, e um grupo de checkboxes com o mesmo name fica só com o último valor — para esse caso, use dados.getAll("atividades").
Forma
Vantagem
Quando usar
querySelector("#id").value
Devolve o elemento, não só o valor
Quando você vai mexer em classes e atributos do campo
form.elements.nome.value
Não depende de id, só de name
Validação genérica, percorrendo campos
new FormData(form)
Pega tudo de uma vez, pronto para enviar
Montar o objeto final depois de validado
1.3 Cada tipo de campo devolve uma coisa diferente¶
JavaScript
// Texto: SEMPRE aplique trim() — " " não é um nome preenchidoconstnome=document.querySelector("#nome").value.trim();// Número: value é SEMPRE string, mesmo em type="number"constidade=Number(document.querySelector("#idade").value);// Checkbox único: booleanoconstaceitouTermos=document.querySelector("#termos").checked;// Radio: pegue o que está marcado (pode não haver nenhum)constvinculo=form.querySelector("input[name='vinculo']:checked")?.value??"";// Grupo de checkboxes: transforme a NodeList em array e extraia os valoresconstatividades=[...form.querySelectorAll("input[name='atividades']:checked")].map((caixa)=>caixa.value);// Select simples e select múltiploconstcurso=document.querySelector("#curso").value;constcursos=[...document.querySelector("#cursos").selectedOptions].map((o)=>o.value);// Arquivo: uma lista de File, mesmo sem o atributo multipleconstcomprovante=document.querySelector("#comprovante").files[0];
⚠️ Atenção
O erro clássico, que aparece o tempo todo: input.value de um type="number" retorna a string"25", não o número 25. Fazer idade + 1 produz "251". Converta sempre com Number() — e lembre-se, da Aula 11, de que Number("") é 0, então teste o campo vazio antes de converter.
O ?. no radio (encadeamento opcional, Aula 11) evita o clássico Cannot read properties of null (reading 'value') quando nenhuma opção está marcada; o ?? completa com uma string vazia.
Validar cedo demais irrita; validar tarde demais frustra. O momento certo depende do evento:
Momento
Evento
Custo de escolher errado
Ao digitar
input
Acusa "e-mail inválido" na segunda letra digitada
Ao sair do campo
blur
O usuário só descobre o erro depois de sair
Ao enviar
submit
Tarde demais como único feedback
A estratégia que dá a melhor experiência combina os três, em quatro regras:
Não valide enquanto o usuário digita pela primeira vez. Ele ainda está escrevendo.
Valide no blur, quando ele sai do campo — o momento natural de "terminei este aqui".
A partir do instante em que o campo já mostrou erro, passe a validar no input. Assim o usuário vê a mensagem sumir enquanto corrige, em vez de precisar sair do campo de novo.
Valide tudo no submit e leve o foco ao primeiro campo inválido.
JavaScript
constcampos=[...form.querySelectorAll("input, select, textarea")];campos.forEach((campo)=>{campo.addEventListener("blur",()=>{validarCampo(campo);campo.dataset.tocado="true";// marca: este campo já foi visitado});campo.addEventListener("input",()=>{if(campo.dataset.tocado==="true")validarCampo(campo);});});
O truque está no dataset.tocado (Aula 13): o próprio DOM guarda o estado "este campo já foi visitado", sem precisar de um array paralelo em JavaScript. Como dataset sempre devolve string, a comparação é com "true", não com true.
💡 Dicablur não borbulha, então form.addEventListener("blur", …) não funciona — é por isso que o código acima registra o ouvinte campo a campo. Se você quiser um ouvinte só, use o evento focusout, que é a versão borbulhante do blur.
O navegador já validou o campo antes de você escrever qualquer linha. A Constraint Validation API é a interface que expõe esse resultado — e é ela que faz required, type="email", min, max, minlength e pattern valerem também para o seu código.
constcampo=document.querySelector("#email");campo.validity.valid;// true se o campo passa em todas as restriçõescampo.validity.valueMissing;// required não preenchidocampo.validity.typeMismatch;// type="email" ou type="url" com formato erradocampo.validity.patternMismatch;// não casou com o atributo patterncampo.validity.tooShort;// abaixo do minlength (tooLong: acima do maxlength)campo.validity.rangeUnderflow;// abaixo do min (rangeOverflow: acima do max)campo.validity.customError;// há uma mensagem definida por setCustomValidity
campo.checkValidity();// true/false, silencioso (não mostra nada na tela)campo.reportValidity();// valida E mostra a mensagem nativa do navegadorcampo.setCustomValidity("As senhas não coincidem.");// define um erro seucampo.setCustomValidity("");// limpa o erro personalizado
setCustomValidity merece cuidado: enquanto a mensagem estiver definida, o campo é considerado inválido para sempre, mesmo que o usuário corrija o valor. Você precisa limpá-la (setCustomValidity("")) no início de cada nova validação. Esquecer isso produz o bug "o formulário nunca envia" mais comum da aula.
Em vez de escrever de novo a regra "e-mail precisa ter arroba", leia o diagnóstico que o navegador já fez e devolva um texto em português decente:
JavaScript
functionmensagemNativa(campo){constv=campo.validity;constrotulo=campo.labels[0]?.textContent.replace("*","").trim()||"Este campo";if(v.valueMissing)return`${rotulo} é obrigatório.`;if(v.typeMismatch)return`${rotulo} está em um formato inválido.`;if(v.tooShort)return`${rotulo} deve ter ao menos ${campo.minLength} caracteres.`;if(v.tooLong)return`${rotulo} deve ter no máximo ${campo.maxLength} caracteres.`;if(v.rangeUnderflow)return`${rotulo} deve ser no mínimo ${campo.min}.`;if(v.rangeOverflow)return`${rotulo} deve ser no máximo ${campo.max}.`;if(v.patternMismatch)returncampo.title||`${rotulo} está em um formato inválido.`;return"";}
campo.labels é uma coleção com todos os <label> associados ao campo — mais uma razão para você ter caprichado no for/id da Aula 03. O ?. protege contra campos sem rótulo, e o campo.title aproveita o atributo title do pattern como mensagem, que é exatamente para isso que ele existe.
A arquitetura final combina duas camadas: os atributos HTML declaram as regras simples e servem de documentação; o JavaScript cuida das regras de negócio (dígitos do CPF, idade mínima, senhas coincidentes, ao menos um minicurso escolhido) e da apresentação das mensagens.
🔬 Investigue
Abra inscricao.html com o Live Server e o Console. Digite const e = document.querySelector("#email") e depois e.validity. Expanda o objeto: todos os campos aparecem como false e valid: true porque o campo está vazio e ainda não é required para o navegador… ou aparece valueMissing: true, se você já colocou required. Agora digite e.value = "ana@" e chame e.validity de novo: typeMismatch virou true. Por fim, chame e.reportValidity() e veja a caixa nativa — a mesma que novalidate esconde.
🧠 Você sabia?
O atributo pattern existe desde o HTML5, mas as âncoras ^ e $ são implícitas nele: o navegador sempre exige correspondência total. Escrever pattern="^\d{5}-?\d{3}$" funciona por acidente (as âncoras extras não atrapalham), mas escrever pattern="/\d{5}-?\d{3}/"nunca casa, porque as barras viram caracteres literais a serem procurados. É o erro nº 1 de quem aprendeu regex em JavaScript antes de usar pattern.
A melhor forma de organizar validação é escrever funções puras: recebem um valor, devolvem a mensagem de erro (string) ou "" quando está tudo certo. Elas não tocam no DOM, não dependem de variáveis globais e por isso podem ser testadas no console, reaproveitadas em outro projeto e lidas sem sustos.
JavaScript
// js/inscricao.js — validadores (funções puras: entram dados, sai mensagem)functionvalidarObrigatorio(valor,rotulo="Este campo"){returnvalor.trim()===""?`${rotulo} é obrigatório.`:"";}functionvalidarTamanho(valor,minimo,maximo,rotulo){constv=valor.trim();if(v.length<minimo)return`${rotulo} deve ter ao menos ${minimo} caracteres.`;if(v.length>maximo)return`${rotulo} deve ter no máximo ${maximo} caracteres.`;return"";}functionvalidarNomeCompleto(valor){constpartes=valor.trim().split(/\s+/);// divide por qualquer sequência de espaçosif(partes.length<2)return"Informe nome e sobrenome.";if(partes.some((parte)=>parte.length<2)){return"Cada parte do nome deve ter ao menos 2 letras.";}return"";}functionvalidarEmail(valor){constv=valor.trim();if(v==="")return"O e-mail é obrigatório.";if(!v.includes("@"))return"O e-mail deve conter @.";const[usuario,dominio]=v.split("@");if(!usuario||!dominio)return"Formato de e-mail inválido.";if(!dominio.includes("."))return"O domínio do e-mail está incompleto.";return"";}
Repare no padrão: cada função devolve uma mensagem, a primeira que se aplica. Mensagens boas dizem o que fazer. "Campo inválido" não ajuda ninguém; "A senha deve conter ao menos uma letra maiúscula" resolve o problema do usuário em um segundo.
Validar uma data digitada como 31/02/2000 exige mais do que formato. Duas armadilhas do objeto Date explicam por quê:
JavaScript
constdata=newDate(2000,1,31);// meses vão de 0 a 11: 1 é fevereirodata.getDate();// 2 ← o Date "conserta" a data sozinho, sem erro nenhumdata.getMonth();// 2 ← virou março
A primeira armadilha é que os meses vão de 0 a 11. A segunda é que Date nunca reclama de uma data inexistente: ele a desloca. A defesa é sempre a mesma — reconstruir dia, mês e ano a partir do objeto criado e comparar com o que foi digitado. Se não bate, a data não existe no calendário. A função completa, com o cálculo da idade, está na Mão na massa.
O CPF tem 11 dígitos, sendo os dois últimos dígitos verificadores calculados a partir dos nove primeiros. Isso significa que um CPF digitado errado é detectável sem consultar nenhum servidor — e é por isso que nenhum site sério aceita 111.111.111-11.
JavaScript
functionvalidarCPF(cpf){constnumeros=cpf.replace(/\D/g,"");// \D = tudo que NÃO é dígitoif(numeros.length!==11)return"O CPF deve ter 11 dígitos.";if(/^(\d)\1{10}$/.test(numeros))return"CPF inválido.";// 000…0, 111…1, 999…9constcalcularDigito=(base,pesoInicial)=>{letsoma=0;for(leti=0;i<base.length;i++){soma=soma+Number(base[i])*(pesoInicial-i);}constresto=(soma*10)%11;returnresto===10?0:resto;};constprimeiro=calcularDigito(numeros.slice(0,9),10);constsegundo=calcularDigito(numeros.slice(0,10),11);if(primeiro!==Number(numeros[9])||segundo!==Number(numeros[10])){return"CPF inválido. Confira os números digitados.";}return"";}
Leia a regex /^(\d)\1{10}$/ com calma, porque ela usa um recurso que a seção 5 vai formalizar: (\d) captura um dígito no grupo 1, e \1{10} exige que o mesmo dígito apareça mais dez vezes. É a forma mais curta de dizer "todos os onze dígitos são iguais".
🧠 Você sabia?
O algoritmo do CPF é uma variação do cálculo de dígito verificador módulo 11, o mesmo princípio usado no ISBN de livros, no código de barras dos boletos bancários e no número de matrícula de muitas universidades. A ideia é antiga e simples: some os dígitos multiplicados por pesos decrescentes, tire o resto da divisão por 11, e guarde o resultado no fim do número. Um dígito trocado ou dois dígitos invertidos mudam a soma e o erro é detectado na hora — sem internet, sem banco de dados.
Uma expressão regular (regex) é um padrão que descreve um conjunto de strings. Com ela você responde perguntas como "este texto tem formato de e-mail?", "quais números aparecem aqui?" ou "troque todo espaço duplo por um só". Regex existe em praticamente todas as linguagens e ferramentas — JavaScript, Python, Java, SQL, grep, a busca do VS Code — e é um dos poucos conhecimentos que você leva inteiro para qualquer tecnologia da carreira.
JavaScript
// 1. Forma literal — preferida quando o padrão é fixoconstpadrao=/abc/;// 2. Construtor — quando o padrão é montado em tempo de execuçãoconsttermoDigitado="café";constbusca=newRegExp(termoDigitado,"gi");
No construtor, a string precisa escapar as barras invertidas em dobro ("\\d" em vez de \d), porque a barra invertida também é especial dentro de strings. Use a forma literal sempre que puder.
. qualquer caractere, exceto quebra de linha
\d dígito [0-9] \D NÃO dígito
\w palavra [A-Za-z0-9_] \W NÃO caractere de palavra
\s espaço, tabulação, quebra \S NÃO espaço em branco
[abc] a, b OU c [^abc] qualquer coisa EXCETO a, b, c
[a-z] letra minúscula [A-Z] letra maiúscula
[0-9] dígito [À-ÿ] letras acentuadas da faixa Latin-1
JavaScript
/[A-Za-zÀ-ÿ]/.test("ção");// true — letra com ou sem acento/[^\d]/.test("1234");// false — só há dígitos
* 0 ou mais
+ 1 ou mais
? 0 ou 1 (opcional)
{n} exatamente n
{n,} n ou mais
{n,m} entre n e m
JavaScript
/^\d{3}$/.test("123");// true — exatamente três dígitos/^\d{2,4}$/.test("12345");// false — passou de quatro/colou?r/.test("colour");// true — o "u" é opcional
Gulosos e preguiçosos. Por padrão, quantificadores são gulosos: pegam o máximo possível. Acrescentar ? os torna preguiçosos.
JavaScript
consttexto="<b>negrito</b> e <i>itálico</i>";texto.match(/<.+>/)[0];// "<b>negrito</b> e <i>itálico</i>" ← gulosotexto.match(/<.+?>/)[0];// "<b>" ← preguiçoso
^ início da string (ou da linha, com a flag m)
$ fim da string
\b limite de palavra
\B NÃO limite de palavra
JavaScript
/^abc/.test("abcdef");// true — começa com abc/abc$/.test("xyzabc");// true — termina com abc/^abc$/.test("abc");// true — é exatamente abc/\bgato\b/.test("gatorade");// false — "gato" precisa ser palavra inteira
📌 Vale gravar
A âncora é o que separa busca de validação. /\d{3}/.test("abc1234xyz") é true: encontrou três dígitos em algum lugar. /^\d{3}$/.test("abc1234xyz") é false: a string inteira precisa ser exatamente três dígitos. Toda regex de validação de formato precisa de ^ no início e $ no fim. Essa questão cai todo semestre.
/(abc)+/;// grupo de captura: "abc" repetido/(?:abc)+/;// grupo SEM captura (não guarda o trecho)/(palestra|minicurso|mesa)/;// alternância: um dos três
Lookahead ((?=…) positivo, (?!…) negativo) e lookbehind ((?<=…) e (?<!…)) verificam o que vem depois ou antes sem consumir os caracteres:
JavaScript
// Senha forte: ao menos 1 minúscula, 1 maiúscula, 1 dígito, 1 especial, mínimo 8constsenhaForte=/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;/\d+(?=\s*vagas)/.exec("Restam 12 vagas")[0];// "12" — seguido de "vagas"/(?<=R\$\s?)\d+([.,]\d{2})?/.exec("Total: R$ 89,90")[0];// "89,90" — precedido de R$
Cada lookahead da senha forte é uma verificação independente feita a partir do início da string: "olhando para a frente, existe em algum ponto uma minúscula?". Todas precisam ser verdadeiras, e só então [A-Za-z\d@$!%*?&#]{8,} consome a string de fato.
// test — devolve booleano. O mais usado em validação./\d/.test("abc123");// true// match — sem a flag g, devolve os grupos capturados"07/05/1998".match(/(\d{2})\/(\d{2})\/(\d{4})/);// ["07/05/1998", "07", "05", "1998", index: 0, ...]// match — com a flag g, devolve só as ocorrências"a1 b2 c3".match(/\d/g);// ["1", "2", "3"]// matchAll — precisa da flag g; devolve um iterável com os grupos de cada ocorrênciafor(constmof"Ana: 8.5, Bruno: 6.0".matchAll(/(?<nome>\w+): (?<nota>[\d.]+)/g)){console.log(m.groups.nome,m.groups.nota);}// replace — $1, $2 referenciam os grupos capturados; uma função permite calcular" muitos espaços ".replace(/\s+/g," ").trim();// "muitos espaços""07/05/1998".replace(/(\d{2})\/(\d{2})\/(\d{4})/,"$3-$2-$1");// "1998-05-07""restam 100".replace(/\d+/,(n)=>Number(n)*2);// "restam 200"// split aceita regex como separador"nome; idade , curso".split(/\s*[;,]\s*/);// ["nome", "idade", "curso"]
O padrão TELEFONE merece uma leitura guiada: \(?\d{2}\)? aceita o DDD com ou sem parênteses; \s? permite um espaço; 9? torna opcional o nono dígito dos celulares; \d{4}-?\d{4} aceita o traço ou a ausência dele. Assim (66) 99999-9999, 66999999999 e (66) 3511-1000 passam, e 9999-9999 (sem DDD) não passa.
// Suficiente para 99,9% dos casos reaisconstEMAIL=/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
Não existe regex simples que valide e-mail perfeitamente. A especificação que define o formato permite construções tão exóticas (comentários entre parênteses, aspas, caracteres acentuados) que a expressão completa passa de seis mil caracteres e é ilegível. A recomendação profissional é usar uma regex simples de formato e confirmar o endereço por um e-mail de verificação. Uma regex agressiva demais rejeita endereços válidos — problema pior do que aceitar um endereço inválido, porque o usuário desiste do cadastro sem entender por quê.
🧠 Você sabia?
As expressões regulares nasceram em 1951, num artigo do matemático Stephen Kleene sobre linguagens regulares — décadas antes da web. O * que você usa em \d* chama-se, até hoje, estrela de Kleene. A notação chegou à programação nos anos 1960, com o editor qed de Ken Thompson, e de lá foi para o grep do Unix (o nome vem de g/re/p, "global / regular expression / print"). Ou seja: você está usando uma ideia de setenta anos para conferir um CPF.
Combine sempre regex (formato) com validação algorítmica (regra), como você fez no CPF da seção 4.2.
ReDoS — travar o navegador com uma regex. Padrões com quantificador dentro de quantificador podem levar tempo exponencial para concluir que algo não casa:
JavaScript
// Perigosa: quantificador aninhadoconstperigosa=/^(a+)+$/;// perigosa.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaX") pode travar a aba// Reescrita segura, com o mesmo efeito práticoconstsegura=/^a+$/;
Evite (x+)+, (x*)* e (x|x)*. E, quando um padrão passar de uma linha, decomponha-o em pedaços nomeados e monte com new RegExp: você não vai lembrar o que aquela sequência de símbolos faz daqui a duas semanas. Sempre comente uma regex não trivial.
🔬 Investigue
Abra o console e teste: /^\d{3}$/.test("123"), depois /^\d{3}$/.test(" 123") — por que o segundo é false? Agora rode "Ana Paula da Silva".split(/\s+/) e conte os elementos. Por fim, vá a https://regex101.com, cole ^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$ no campo da expressão, escolha o sabor ECMAScript (JavaScript) e teste com (66) 99988-7766, 66999887766 e 9998-7766. O painel da direita explica token por token o que cada símbolo faz — é a melhor forma de aprender regex que existe.
Máscara é a formatação que aparece enquanto o usuário digita: 12345678909 vira 123.456.789-09. Ela reduz erros de digitação e deixa claro qual formato o campo espera.
JavaScript
// js/inscricao.js — máscarasfunctionmascaraCPF(valor){returnvalor.replace(/\D/g,"")// fica só com os dígitos.slice(0,11)// no máximo 11.replace(/(\d{3})(\d)/,"$1.$2")// primeiro ponto.replace(/(\d{3})(\d)/,"$1.$2")// segundo ponto.replace(/(\d{3})(\d{1,2})$/,"$1-$2");// traço antes dos verificadores}// Aplicação: reescreve o valor do campo a cada tecladocument.querySelector("#cpf").addEventListener("input",(evento)=>{evento.target.value=mascaraCPF(evento.target.value);});
A receita é sempre a mesma: limpe tudo o que não é dígito, corte no comprimento máximo e insira os separadores com replace e grupos capturados. As máscaras de telefone e de data seguem exatamente esse molde e estão completas na Mão na massa.
⚠️ Atenção
Máscaras atrapalham quem cola um valor já formatado e quem usa leitor de tela (o campo muda sozinho enquanto a pessoa digita). Duas regras compensam isso: sempre remova a formatação (valor.replace(/\D/g, "")) antes de validar e antes de enviar ao servidor, e nunca use a máscara como única indicação do formato — coloque também um texto de ajuda visível, ligado ao campo por aria-describedby.
Uma mensagem de erro só cumpre seu papel se todo mundo a percebe: quem enxerga, quem não distingue vermelho de verde e quem usa leitor de tela.
JavaScript
// js/inscricao.js — exibição do estado de um campofunctionmostrarErro(campo,mensagem){constalvo=document.querySelector(`#erro-${campo.id}`);if(mensagem){campo.classList.add("invalido");campo.classList.remove("valido");campo.setAttribute("aria-invalid","true");campo.setAttribute("aria-describedby",`erro-${campo.id}`);alvo.textContent=mensagem;}else{campo.classList.remove("invalido");campo.classList.add("valido");campo.setAttribute("aria-invalid","false");campo.removeAttribute("aria-describedby");alvo.textContent="";}}
css/estilo.css — estados do formulário
CSS
/* ===== Formulário validado ===== */.campo{display:grid;gap:0.25rem;margin-bottom:1.25rem;}.campoinput,.camposelect,.campotextarea{width:100%;padding:0.625rem0.75rem;border:2pxsolidvar(--cor-borda,#ccc);border-radius:6px;font:inherit;transition:border-color0.2s;}.campoinput:focus-visible,.camposelect:focus-visible{outline:3pxsolidrgba(26,127,181,0.4);outline-offset:1px;border-color:var(--cor-secundaria,#1a7fb5);}.campo.invalido{border-color:#c0392b;}.campo.valido{border-color:#27ae60;}.erro{display:block;min-height:1.2em;/* reserva espaço: evita o layout "pular" ao exibir o erro */color:#c0392b;font-size:0.875rem;}.erro:not(:empty)::before{content:"⚠ ";/* o erro não depende só da cor */}@media(prefers-reduced-motion:reduce){.campoinput,.camposelect{transition:none;}}
Os cinco requisitos de acessibilidade em mensagens de erro:
role="alert" no elemento da mensagem: leitores de tela anunciam o conteúdo assim que ele aparece, sem o usuário precisar navegar até lá.
aria-invalid="true" no campo: o leitor anuncia "inválido" ao entrar no campo.
aria-describedby ligando o campo à mensagem: o texto do erro é lido junto com o rótulo.
Nunca sinalize erro só pela cor. Some texto e um símbolo (o ::before do CSS acima).
Reserve espaço para a mensagem com min-height. Sem isso, o layout salta quando o erro aparece e o usuário perde a referência visual — e quem usa ampliador de tela se perde de vez.
Note que o forEach valida todos os campos antes de parar: o usuário vê de uma vez tudo o que precisa corrigir, e não um erro por tentativa. O focus() no primeiro inválido é obrigatório para quem navega por teclado — sem ele, a pessoa aperta "Enviar", nada acontece e ela não faz ideia de onde está o problema.
⚠️ Atenção
Toda a validação desta aula acontece no navegador, e o navegador é do usuário. Qualquer pessoa abre o DevTools, apaga o required e envia o formulário; ou envia a requisição direto, sem passar pela sua página. Validação no cliente serve para experiência (avisar cedo, evitar ida e volta ao servidor); a validação que serve para segurança é a do servidor, e você a construirá no Nível 2. Nunca confie em dados que vieram do navegador.
O navegador oferece um armazenamento simples, que sobrevive ao fechamento da aba: o localStorage. Ele guarda pares chave/valor, sempre em texto.
JavaScript
// EscreverlocalStorage.setItem("inscricao:rascunho",JSON.stringify({nome:"Ana",curso:"SI"}));// Ler (devolve null quando a chave não existe)constbruto=localStorage.getItem("inscricao:rascunho");constrascunho=bruto?JSON.parse(bruto):null;// Remover uma chave e limpar tudolocalStorage.removeItem("inscricao:rascunho");localStorage.clear();
Como só há texto, objetos e arrays precisam de JSON.stringify na ida e JSON.parse na volta. Guardar { nome: "Ana" } sem stringify armazena a string "[object Object]" — outro clássico da primeira semana.
Recurso
Vive até
Cabe
localStorage
Ser apagado pelo código ou pelo usuário
Cerca de 5 MB por origem
sessionStorage
A aba ser fechada
Cerca de 5 MB por aba
Variável em JavaScript
A página recarregar
A memória disponível
⚠️ AtençãolocalStorage é legível por qualquer script da mesma origem e fica no computador, muitas vezes compartilhado. Nunca guarde senha, CPF, token de acesso ou dado de cartão. No rascunho da inscrição vamos salvar apenas nome, e-mail e curso — CPF e telefone ficam de fora, de propósito.
🔬 Investigue
Abra qualquer página do site, vá ao DevTools na aba Application (Chrome) ou Armazenamento (Firefox) e clique em Local Storage → a URL do seu site. Rode no console localStorage.setItem("teste", "olá") e veja a linha aparecer na tabela em tempo real. Feche o navegador inteiro, abra de novo, volte à página e rode localStorage.getItem("teste"): o valor continua lá. Agora abra a mesma página em uma janela anônima e repita o getItem: devolve null, porque a janela anônima é outra área de armazenamento.
9. Consultas dinâmicas: busca, filtro e ordenação¶
A segunda metade da aula responde a outra pergunta: como deixar o usuário encontrar o que procura numa lista? A resposta é sempre a mesma arquitetura da Aula 13 — estado → dados → renderização → eventos —, com o estado agora guardando o termo buscado, o filtro escolhido e o critério de ordenação.
A função que produz a lista visível é sempre a mesma sequência: cópia → busca → filtro → ordenação.
JavaScript
// Estado da consulta (os nomes dos campos vêm do js/dados.js da Aula 12)lettermoBusca="";letareaAtual="todas";letordenacaoAtual="hora";functionobterPalestrasVisiveis(){letresultado=[...palestras];// 1. cópia: sort() modifica o array originalif(termoBusca){// 2. busca por textoconsttermo=normalizar(termoBusca);resultado=resultado.filter((p)=>normalizar(p.titulo).includes(termo));}if(areaAtual!=="todas"){// 3. filtro por categoriaresultado=resultado.filter((p)=>p.area===areaAtual);}constordenadores={// 4. ordenaçãohora:(a,b)=>a.dia-b.dia||a.hora.localeCompare(b.hora),titulo:(a,b)=>a.titulo.localeCompare(b.titulo,"pt-BR"),vagas:(a,b)=>b.vagas-b.inscritos-(a.vagas-a.inscritos),};resultado.sort(ordenadores[ordenacaoAtual]);returnresultado;}
Três detalhes que separam código que funciona de código que funciona sempre:
[...palestras] cria uma cópia. sort() reordena o array original; sem a cópia, a ordem dos dados mudaria a cada renderização e o filtro seguinte partiria de outra base. É um bug sutil, difícil de encontrar e frequente.
localeCompare(b, "pt-BR") ordena corretamente palavras acentuadas. O sort() puro compara códigos de caractere e coloca "Ávila" depois de "Zampieri".
a.dia - b.dia || a.hora.localeCompare(b.hora) é ordenação em dois níveis: quando os dias são iguais, a subtração dá 0 (falso) e o || passa para o critério de desempate. Repare no nome do campo: hora, como no dados.js da Aula 12. Inventar um horario aqui daria undefined.localeCompare — TypeError na primeira ordenação.
functionnormalizar(texto){returntexto.normalize("NFD")// separa a letra do acento combinante.replace(/[̀-ͯ]/g,"")// remove os acentos combinantes.toLowerCase().trim();}normalizar("Segurança");// "seguranca"normalizar("CAFÉ");// "cafe"
Na forma de normalização NFD, o caractere "é" é decomposto em "e" mais um acento combinante separado. Esses acentos vivem no intervalo Unicode ̀–ͯ, e uma única regex com a flag g remove todos de uma vez. Aplique normalizar nos dois lados da comparação: no termo digitado e no texto pesquisado. Sem isso, quem digita "seguranca" no celular não encontra "Segurança" — e desiste.
Digitar "acessibilidade" dispara catorze eventos input. Filtrar e redesenhar catorze vezes trava a interface em listas grandes. O debounce da Aula 13 resolve: a função só executa quando os disparos param por um tempo.
Toda listagem filtrável precisa de duas coisas que iniciantes esquecem:
Uma mensagem de estado vazio que diga o que fazer ("Nenhuma atividade encontrada para 'xyz'. Tente outro termo ou limpe os filtros."), e não uma tela em branco.
Um contador anunciado (aria-live="polite") informando quantos resultados apareceram. Quem usa leitor de tela não vê a lista encolher; sem o contador, digitar na busca não produz nenhum retorno perceptível.
🔎 Por baixo do capôaria-live="polite" cria uma região viva: o leitor de tela passa a observar aquele elemento e anuncia mudanças de conteúdo assim que terminar de ler o que estava lendo. assertive interrompe na hora — reserve para erros graves. A região precisa existir no HTML antes de ser preenchida: se você criar o elemento e já colocar o texto dentro, muitos leitores não anunciam nada, porque não havia região para observar. É por isso que o <p> vazio já vem no HTML.
💻 Mão na massa — Inscrição validada e programação com busca¶
Ao fim destes oito passos, o site da Semana Acadêmica de Sistemas de Informação terá um formulário de inscrição que valida campo a campo com mensagens acessíveis, salva rascunho e mostra confirmação; e uma página de programação com busca em tempo real, filtro por área e ordenação — tudo sobre o array palestras do js/dados.js da Aula 12, sem mudar uma linha daquele arquivo.
⚠️ Atençãojs/dados.js é a fonte única de dados do projeto desde a Aula 12, e o esquema dele é lei: palestras com area, hora e palestranteId; o array palestrantes; o dicionário nomesDasAreas. O js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) já dependem desses nomes. Hoje você adapta a consulta aos dados, não os dados à consulta.
Primeiro o <head>, com os quatro scripts na ordem que importa — defer executa na ordem das tags, e inscricao.js precisa que debounce (do app.js, Passo 4) já exista:
inscricao.html — <head> completo
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Inscrição gratuita na Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Inscrição — 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/inscricao.js"defer></script></head>
Agora o <main>. A estrutura repete o mesmo bloco para cada campo: <label>, campo, <span class="erro" role="alert">.
⚠️ Atenção
A <section class="vagas"> das Aulas 10 e 11 — com #vagas-restantes, #vagas-totais, #percentual-ocupacao, #aviso-vagas e #valor-taxa — fica onde está, acima do formulário. É ela que mostra o "Últimas vagas!" exigido no Checkpoint da Aula 11, e o código que a alimenta continua no js/inscricao.js (você o amplia no Passo 3, não o substitui). O que muda de lugar é só o <form>.
inscricao.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Inscrição</h1><p>Preencha os dados abaixo. Os campos marcados com * são obrigatórios.</p><!-- Seção de vagas das Aulas 10 e 11: continua exatamente como estava --><sectionclass="vagas"><h2>Vagas</h2><p>
Restam <strongid="vagas-restantes">—</strong> de
<spanid="vagas-totais">—</span> vagas
(<spanid="percentual-ocupacao">—</span>% ocupadas).
</p><pid="aviso-vagas"class="aviso"role="status"></p><pclass="taxa">
Taxa de inscrição: <strongid="valor-taxa">—</strong><spanid="observacao-taxa"></span></p></section><pid="aviso-formulario"class="aviso-formulario"role="status"></p><formid="form-inscricao"novalidate><fieldset><legend>Seus dados</legend><divclass="campo"><labelfor="nome">Nome completo *</label><inputtype="text"id="nome"name="nome"autocomplete="name"requiredminlength="5"maxlength="80"><spanclass="erro"id="erro-nome"role="alert"></span></div><divclass="campo"><labelfor="email">E-mail *</label><inputtype="email"id="email"name="email"autocomplete="email"required><spanclass="erro"id="erro-email"role="alert"></span></div><divclass="campo"><labelfor="cpf">CPF *</label><inputtype="text"id="cpf"name="cpf"inputmode="numeric"maxlength="14"placeholder="000.000.000-00"required><spanclass="erro"id="erro-cpf"role="alert"></span></div><divclass="campo"><labelfor="telefone">Telefone com DDD *</label><inputtype="text"id="telefone"name="telefone"inputmode="tel"maxlength="15"placeholder="(66) 99999-9999"required><spanclass="erro"id="erro-telefone"role="alert"></span></div><divclass="campo"><labelfor="nascimento">Data de nascimento *</label><inputtype="text"id="nascimento"name="nascimento"inputmode="numeric"maxlength="10"placeholder="dd/mm/aaaa"required><spanclass="erro"id="erro-nascimento"role="alert"></span></div></fieldset><fieldset><legend>Participação</legend><divclass="campo"><labelfor="curso">Curso ou vínculo *</label><selectid="curso"name="curso"required><optionvalue="">Selecione…</option><optionvalue="si">Sistemas de Informação</option><optionvalue="ads">Análise e Desenvolvimento de Sistemas</option><optionvalue="eng">Engenharias</option><optionvalue="outro">Outro curso</option><optionvalue="externo">Comunidade externa</option></select><spanclass="erro"id="erro-curso"role="alert"></span></div><divclass="campo"><spanclass="rotulo-grupo"id="rotulo-atividades">Atividades de interesse *</span><divclass="grupo-caixas"role="group"aria-labelledby="rotulo-atividades"><label><inputtype="checkbox"name="atividades"value="git"> Minicurso de Git</label><label><inputtype="checkbox"name="atividades"value="acessibilidade"> Minicurso de acessibilidade</label><label><inputtype="checkbox"name="atividades"value="maratona"> Maratona de programação</label><label><inputtype="checkbox"name="atividades"value="palestras"> Somente palestras</label></div><spanclass="erro"id="erro-atividades"role="alert"></span></div><divclass="campo"><labelclass="linha"><inputtype="checkbox"id="termos"name="termos"required>
Li e aceito o regulamento do evento *</label><spanclass="erro"id="erro-termos"role="alert"></span></div></fieldset><divclass="acoes-formulario"><buttontype="submit"class="botao">Enviar inscrição</button><buttontype="button"class="botao botao--contorno"id="limpar-rascunho">Limpar rascunho</button></div></form></main>
Quatro decisões que valem nota: novalidate no formulário (as mensagens são nossas), inputmode nos campos numéricos (o celular abre o teclado certo sem mudar o type, como na Aula 04), o grupo de checkboxes dentro de um role="group" com aria-labelledby — assim o leitor de tela anuncia "Atividades de interesse, grupo" antes das opções — e a classe .aviso-formulario no parágrafo de resposta do envio. Ela não é a .aviso da Aula 11: aquela é a caixa vermelha de "Últimas vagas!", com .aviso:empty { display: none }, e continua sendo usada pelo #aviso-vagas logo acima. Dois componentes diferentes, dois nomes diferentes — reaproveitar o nome faria as duas regras brigarem no mesmo estilo.css.
O js/inscricao.js já existe desde a Aula 10 e cresceu na Aula 11: é ele que calcula as vagas restantes, o percentual de ocupação, o aviso "Últimas vagas!" e o valor da taxa. Esse bloco não sai — ele continua no topo do arquivo, sob o comentário // ===== VAGAS (Aulas 10 e 11) =====, e tudo o que você escreve hoje vem depois dele.
Este arquivo usa a função debounce para salvar o rascunho; ela é declarada no js/app.js no Passo 4, que toda página já carrega antes deste script.
js/inscricao.js (topo do arquivo — o que já existe, apenas identificado com um comentário)
JavaScript
// js/inscricao.js — vagas (Aulas 10 e 11) e validação do formulário (Aula 14)// ===== VAGAS (Aulas 10 e 11) =====// As constantes VAGAS_TOTAIS e INSCRITOS, o cálculo do percentual, o aviso// de "Últimas vagas!" e o valor da taxa continuam exatamente como estavam.// Nada abaixo depende deles, e nada deles depende do que vem abaixo.
js/inscricao.js (acrescente a partir daqui)
JavaScript
// ===== ESTADO =====constCHAVE_RASCUNHO="inscricao:rascunho";constCAMPOS_DO_RASCUNHO=["nome","email","curso"];// sem CPF e sem telefone, de propósito// ===== ELEMENTOS =====constels={form:document.querySelector("#form-inscricao"),aviso:document.querySelector("#aviso-formulario"),limpar:document.querySelector("#limpar-rascunho"),};// ===== PADRÕES =====constPADROES={telefone:/^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$/,email:/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,data:/^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/,};// ===== VALIDADORES (funções puras) =====functionvalidarNomeCompleto(valor){constpartes=valor.trim().split(/\s+/);if(valor.trim()==="")return"O nome completo é obrigatório.";if(partes.length<2)return"Informe nome e sobrenome.";if(partes.some((parte)=>parte.length<2)){return"Cada parte do nome deve ter ao menos 2 letras.";}return"";}functionvalidarEmail(valor){constv=valor.trim();if(v==="")return"O e-mail é obrigatório.";if(!PADROES.email.test(v))return"Informe um e-mail no formato nome@dominio.br.";return"";}functionvalidarTelefone(valor){constv=valor.trim();if(v==="")return"O telefone é obrigatório.";if(!PADROES.telefone.test(v))return"Informe o telefone com DDD, como (66) 99999-9999.";return"";}functionvalidarCPF(cpf){constnumeros=cpf.replace(/\D/g,"");if(numeros==="")return"O CPF é obrigatório.";if(numeros.length!==11)return"O CPF deve ter 11 dígitos.";if(/^(\d)\1{10}$/.test(numeros))return"CPF inválido.";constcalcularDigito=(base,pesoInicial)=>{letsoma=0;for(leti=0;i<base.length;i++){soma=soma+Number(base[i])*(pesoInicial-i);}constresto=(soma*10)%11;returnresto===10?0:resto;};constprimeiro=calcularDigito(numeros.slice(0,9),10);constsegundo=calcularDigito(numeros.slice(0,10),11);if(primeiro!==Number(numeros[9])||segundo!==Number(numeros[10])){return"CPF inválido. Confira os números digitados.";}return"";}functioncalcularIdade(dataTexto){constpartes=dataTexto.trim().split("/");if(partes.length!==3)returnnull;constdia=Number(partes[0]);constmes=Number(partes[1]);constano=Number(partes[2]);constdata=newDate(ano,mes-1,dia);constexiste=data.getFullYear()===ano&&data.getMonth()===mes-1&&data.getDate()===dia;if(!existe)returnnull;consthoje=newDate();letanos=hoje.getFullYear()-ano;constjaFez=hoje.getMonth()>mes-1||(hoje.getMonth()===mes-1&&hoje.getDate()>=dia);if(!jaFez)anos=anos-1;returnanos;}functionvalidarNascimento(valor){constv=valor.trim();if(v==="")return"A data de nascimento é obrigatória.";if(!PADROES.data.test(v))return"Use o formato dd/mm/aaaa.";constanos=calcularIdade(v);if(anos===null)return"Essa data não existe no calendário.";if(anos<0)return"A data de nascimento não pode estar no futuro.";if(anos<16)return"É necessário ter ao menos 16 anos para se inscrever.";if(anos>120)return"Confira o ano digitado.";return"";}functionvalidarCurso(valor){returnvalor===""?"Escolha o seu curso ou vínculo.":"";}functionvalidarAtividades(){constmarcadas=els.form.querySelectorAll("input[name='atividades']:checked");returnmarcadas.length===0?"Escolha ao menos uma atividade.":"";}functionvalidarTermos(marcado){returnmarcado?"":"É preciso aceitar o regulamento para se inscrever.";}// ===== MÁSCARAS =====functionmascaraCPF(valor){returnvalor.replace(/\D/g,"").slice(0,11).replace(/(\d{3})(\d)/,"$1.$2").replace(/(\d{3})(\d)/,"$1.$2").replace(/(\d{3})(\d{1,2})$/,"$1-$2");}functionmascaraTelefone(valor){returnvalor.replace(/\D/g,"").slice(0,11).replace(/(\d{2})(\d)/,"($1) $2").replace(/(\d{5})(\d)/,"$1-$2");}functionmascaraData(valor){returnvalor.replace(/\D/g,"").slice(0,8).replace(/(\d{2})(\d)/,"$1/$2").replace(/(\d{2})(\d)/,"$1/$2");}// ===== EXIBIÇÃO =====functionmostrarErro(campo,mensagem){constalvo=document.querySelector(`#erro-${campo.id}`);if(!alvo)return;if(mensagem){campo.classList.add("invalido");campo.classList.remove("valido");campo.setAttribute("aria-invalid","true");campo.setAttribute("aria-describedby",`erro-${campo.id}`);alvo.textContent=mensagem;}else{campo.classList.remove("invalido");campo.classList.add("valido");campo.setAttribute("aria-invalid","false");campo.removeAttribute("aria-describedby");alvo.textContent="";}}functionmostrarAviso(texto,tipo){els.aviso.textContent=texto;els.aviso.className=texto?`aviso-formulario ${tipo}`:"aviso-formulario";}// ===== ORQUESTRAÇÃO =====functionvalidarCampo(campo){letmensagem="";switch(campo.id){case"nome":mensagem=validarNomeCompleto(campo.value);break;case"email":mensagem=validarEmail(campo.value);break;case"cpf":mensagem=validarCPF(campo.value);break;case"telefone":mensagem=validarTelefone(campo.value);break;case"nascimento":mensagem=validarNascimento(campo.value);break;case"curso":mensagem=validarCurso(campo.value);break;case"termos":mensagem=validarTermos(campo.checked);break;default:mensagem="";}if(campo.name==="atividades"){constgrupo=document.querySelector("#erro-atividades");grupo.textContent=validarAtividades();returngrupo.textContent==="";}mostrarErro(campo,mensagem);returnmensagem==="";}functioncamposValidaveis(){return[...els.form.querySelectorAll("#nome, #email, #cpf, #telefone, #nascimento, #curso, #termos")];}// ===== RASCUNHO (localStorage) =====functionsalvarRascunho(){constdados={};CAMPOS_DO_RASCUNHO.forEach((nome)=>{dados[nome]=els.form.elements[nome].value;});localStorage.setItem(CHAVE_RASCUNHO,JSON.stringify(dados));}functionrestaurarRascunho(){constbruto=localStorage.getItem(CHAVE_RASCUNHO);if(!bruto)return;constdados=JSON.parse(bruto);CAMPOS_DO_RASCUNHO.forEach((nome)=>{if(dados[nome])els.form.elements[nome].value=dados[nome];});mostrarAviso("Recuperamos o rascunho que você tinha começado.","sucesso");}functionlimparRascunho(){localStorage.removeItem(CHAVE_RASCUNHO);els.form.reset();camposValidaveis().forEach((campo)=>{campo.classList.remove("valido","invalido");campo.removeAttribute("aria-invalid");mostrarErro(campo,"");});document.querySelector("#erro-atividades").textContent="";mostrarAviso("Rascunho apagado.","sucesso");}// ===== EVENTOS =====functionregistrarEventos(){// Máscarasdocument.querySelector("#cpf").addEventListener("input",(e)=>{e.target.value=mascaraCPF(e.target.value);});document.querySelector("#telefone").addEventListener("input",(e)=>{e.target.value=mascaraTelefone(e.target.value);});document.querySelector("#nascimento").addEventListener("input",(e)=>{e.target.value=mascaraData(e.target.value);});// Valide no blur; depois do primeiro erro, valide também no inputcamposValidaveis().forEach((campo)=>{campo.addEventListener("blur",()=>{validarCampo(campo);campo.dataset.tocado="true";});campo.addEventListener("input",()=>{if(campo.dataset.tocado==="true")validarCampo(campo);});});// Grupo de checkboxes: um ouvinte só, por delegaçãoels.form.addEventListener("change",(e)=>{if(e.target.name==="atividades")validarCampo(e.target);});// Rascunho: salva 500 ms depois de o usuário parar de digitarels.form.addEventListener("input",debounce(salvarRascunho,500));els.limpar.addEventListener("click",limparRascunho);// Envioels.form.addEventListener("submit",(e)=>{e.preventDefault();letprimeiroInvalido=null;camposValidaveis().forEach((campo)=>{constok=validarCampo(campo);campo.dataset.tocado="true";if(!ok&&!primeiroInvalido)primeiroInvalido=campo;});consterroAtividades=validarAtividades();document.querySelector("#erro-atividades").textContent=erroAtividades;if(primeiroInvalido||erroAtividades){mostrarAviso("Confira os campos destacados antes de enviar.","falha");constalvo=primeiroInvalido||document.querySelector("input[name='atividades']");alvo.focus();alvo.scrollIntoView({behavior:"smooth",block:"center"});return;}constdados=Object.fromEntries(newFormData(els.form).entries());dados.atividades=newFormData(els.form).getAll("atividades");dados.cpf=dados.cpf.replace(/\D/g,"");// sem máscara, como o servidor esperadados.telefone=dados.telefone.replace(/\D/g,"");console.log("Inscrição válida:",dados);localStorage.removeItem(CHAVE_RASCUNHO);els.form.reset();camposValidaveis().forEach((campo)=>campo.classList.remove("valido","invalido"));mostrarAviso(`Inscrição de ${dados.nome} registrada! Você receberá a confirmação em ${dados.email}.`,"sucesso");});}// ===== INICIALIZAÇÃO =====functioniniciar(){if(!els.form)return;registrarEventos();restaurarRascunho();}iniciar();
Passo 4 — debounce disponível em todas as páginas¶
O debounce é usado pelo rascunho e, no próximo passo, pela busca. Como todas as páginas carregam js/app.jsantes do script específico (os dois com defer, e defer preserva a ordem das tags), basta declarar a função lá uma vez. Confira que o <script src="js/app.js" defer> está mesmo no <head> das cinco páginas — inclusive em programacao.html, que vai usar o debounce no Passo 7. Sem ele, a busca morre no primeiro caractere digitado com Uncaught ReferenceError: debounce is not defined.
js/app.js — acrescente ao fim
JavaScript
// Utilitário compartilhado: adia a execução até os disparos pararem por X ms.// Como este arquivo é carregado antes dos scripts de página, a função fica// disponível para inscricao.js e programacao.js.functiondebounce(fn,atraso=300){lettemporizador;returnfunction(...args){clearTimeout(temporizador);temporizador=setTimeout(()=>fn.apply(this,args),atraso);};}
Não há arquivo novo aqui: o js/dados.js da Aula 12 já tem tudo de que a consulta precisa. Abra-o e confirme os três nomes que os próximos passos vão usar:
O campo do horário chama hora, não horario. Um p.horario devolve undefined, e undefined.localeCompare(…) derruba a ordenação inteira com TypeError.
A palestra não guarda o nome de quem apresenta, só o palestranteId. Para buscar por nome, você precisa cruzar as duas listas — exatamente o que o relatório 7 da Aula 12 fazia com find. É a mesma ideia de chave estrangeira que você verá no Nível 2.
Se você sentir vontade de "só ajustar um campinho" no dados.js para simplificar o filtro, resista: o js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) leem esse mesmo arquivo e quebram junto. Adaptar o consumidor ao dado é barato; adaptar o dado a um consumidor é como se perde a fonte única.
Passo 6 — a interface de consulta em programacao.html¶
Troque os cartões escritos à mão (Aula 07) por um contêiner vazio e os controles de consulta. Antes, o <head> com os cinco scripts:
programacao.html — <head> completo
HTML
<head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metaname="description"content="Programação completa da Semana Acadêmica de Sistemas de Informação."><metaname="author"content="Semana Acadêmica de Sistemas de Informação"><title>Programação — 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/relatorios.js"defer></script><scriptsrc="js/programacao.js"defer></script></head>
O relatorios.js da Aula 12 continua aqui: ele só escreve no Console e não atrapalha nada. E repare que js/app.js vem antes de js/programacao.js: é dele que sai o debounce do Passo 4.
programacao.html — conteúdo do <main>
HTML
<mainid="conteudo"tabindex="-1"class="container"><h1>Programação</h1><p>Três dias de palestras, minicursos e maratona. Use a busca e os filtros para achar sua atividade.</p><formclass="consulta"id="consulta-programacao"role="search"><divclass="campo"><labelfor="busca">Buscar por título ou palestrante</label><inputtype="search"id="busca"name="busca"placeholder="Ex.: acessibilidade"autocomplete="off"></div><divclass="campo"><labelfor="area">Área</label><selectid="area"name="area"><optionvalue="todas">Todas as áreas</option><optionvalue="web">Desenvolvimento Web</option><optionvalue="dados">Ciência de Dados</option><optionvalue="seguranca">Segurança</option><optionvalue="ia">Inteligência Artificial</option></select></div><divclass="campo"><labelfor="ordenacao">Ordenar por</label><selectid="ordenacao"name="ordenacao"><optionvalue="hora">Dia e horário</option><optionvalue="titulo">Título (A–Z)</option><optionvalue="vagas">Vagas restantes</option></select></div><buttontype="button"class="botao botao--contorno"id="limpar-consulta">Limpar filtros</button></form><pid="contador-programacao"class="contador"role="status"aria-live="polite"></p><ulid="lista-programacao"class="cartoes"></ul></main>
O <form role="search"> não envia nada: ele existe para agrupar os controles semanticamente (leitores de tela anunciam "busca") e para o reset() do botão "Limpar filtros" funcionar de graça. Os cinco <script> precisam estar na ordem do <head> acima: dados.js declara palestras e palestrantes, app.js declara debounce, e só então programacao.js usa os três.
Ele consome palestras, palestrantes e nomesDasAreas do js/dados.js — sem redeclarar nada — e debounce do js/app.js. Os nomes das constantes e funções levam o sufixo Programacao pelo mesmo motivo da Aula 13: scripts sem type="module" dividem o escopo global, e um els ou um renderizar genérico colide com o do arquivo vizinho no dia em que as duas páginas carregarem os dois.
js/programacao.js
JavaScript
// js/programacao.js — busca, filtro e ordenação da programação.// Depende de js/dados.js (palestras, palestrantes, nomesDasAreas) e de// js/app.js (debounce), ambos carregados antes deste arquivo.// ===== ESTADO =====lettermoBusca="";letareaAtualProgramacao="todas";letordenacaoAtual="hora";// ===== ELEMENTOS =====constelementosProgramacao={form:document.querySelector("#consulta-programacao"),busca:document.querySelector("#busca"),area:document.querySelector("#area"),ordenacao:document.querySelector("#ordenacao"),limpar:document.querySelector("#limpar-consulta"),lista:document.querySelector("#lista-programacao"),contador:document.querySelector("#contador-programacao"),};// ===== FUNÇÕES DE DADOS =====functionnormalizar(texto){returntexto.normalize("NFD").replace(/[̀-ͯ]/g,"").toLowerCase().trim();}/** * Cruza a palestra com o array de palestrantes pelo palestranteId. * É a mesma junção do relatório 7 da Aula 12: `find` + `?.` + `??`. * @param {Object} palestra - item de `palestras` * @returns {string} o nome de quem apresenta, ou "A definir" */functionnomeDoPalestrante(palestra){constpessoa=palestrantes.find((p)=>p.id===palestra.palestranteId);returnpessoa?.nome??"A definir";}functionvagasRestantes(palestra){returnpalestra.vagas-palestra.inscritos;}functionobterPalestrasVisiveis(){letresultado=[...palestras];// cópia: sort() altera o array originalif(termoBusca){consttermo=normalizar(termoBusca);resultado=resultado.filter((p)=>normalizar(p.titulo).includes(termo)||normalizar(nomeDoPalestrante(p)).includes(termo)||normalizar(p.local).includes(termo));}if(areaAtualProgramacao!=="todas"){resultado=resultado.filter((p)=>p.area===areaAtualProgramacao);}constordenadores={hora:(a,b)=>a.dia-b.dia||a.hora.localeCompare(b.hora),titulo:(a,b)=>a.titulo.localeCompare(b.titulo,"pt-BR"),vagas:(a,b)=>vagasRestantes(b)-vagasRestantes(a),};resultado.sort(ordenadores[ordenacaoAtual]);returnresultado;}// ===== RENDERIZAÇÃO =====functioncriarCartaoDePalestra(palestra){constitem=document.createElement("li");item.classList.add("cartao");item.dataset.id=palestra.id;constrestantes=vagasRestantes(palestra);if(restantes===0){constselo=document.createElement("span");selo.classList.add("cartao__selo");selo.textContent="Esgotado";item.appendChild(selo);}consttitulo=document.createElement("h2");titulo.textContent=palestra.titulo;constmeta=document.createElement("p");meta.classList.add("cartao__meta");meta.textContent=`Dia ${palestra.dia} · ${palestra.hora} · ${palestra.local}`;constquem=document.createElement("p");quem.textContent=nomeDoPalestrante(palestra);constetiqueta=document.createElement("span");etiqueta.classList.add("etiqueta");etiqueta.textContent=nomesDasAreas[palestra.area];constvagas=document.createElement("p");vagas.classList.add("cartao__vagas");vagas.textContent=restantes===0?"Sem vagas — entre na lista de espera":`${restantes} vagas restantes`;item.append(titulo,meta,quem,etiqueta,vagas);returnitem;}functionrenderizarProgramacao(){constvisiveis=obterPalestrasVisiveis();elementosProgramacao.lista.innerHTML="";if(visiveis.length===0){constvazio=document.createElement("li");vazio.classList.add("vazio");vazio.textContent=termoBusca?`Nenhuma atividade encontrada para "${termoBusca}". Tente outro termo ou limpe os filtros.`:"Nenhuma atividade nesta área. Escolha outra ou limpe os filtros.";elementosProgramacao.lista.appendChild(vazio);}else{constfragmento=document.createDocumentFragment();visiveis.forEach((p)=>fragmento.appendChild(criarCartaoDePalestra(p)));elementosProgramacao.lista.appendChild(fragmento);}elementosProgramacao.contador.textContent=visiveis.length===1?"1 atividade encontrada":`${visiveis.length} atividades encontradas`;}// ===== EVENTOS =====functionregistrarEventosDaProgramacao(){elementosProgramacao.busca.addEventListener("input",debounce((e)=>{termoBusca=e.target.value;renderizarProgramacao();},300));elementosProgramacao.area.addEventListener("change",(e)=>{areaAtualProgramacao=e.target.value;renderizarProgramacao();});elementosProgramacao.ordenacao.addEventListener("change",(e)=>{ordenacaoAtual=e.target.value;renderizarProgramacao();});elementosProgramacao.limpar.addEventListener("click",()=>{elementosProgramacao.form.reset();termoBusca="";areaAtualProgramacao="todas";ordenacaoAtual="hora";renderizarProgramacao();elementosProgramacao.busca.focus();});}// ===== INICIALIZAÇÃO =====functioniniciarProgramacao(){if(!elementosProgramacao.lista)return;registrarEventosDaProgramacao();renderizarProgramacao();}iniciarProgramacao();
Abra inscricao.html com o Live Server. Clique no campo "Nome completo", saia sem digitar: aparece "O nome completo é obrigatório." em vermelho, com o símbolo de alerta, e a borda fica vermelha. Digite "Ana": a mensagem muda para "Informe nome e sobrenome." enquanto você digita — porque o campo já foi tocado.
Digite qualquer sequência de 11 dígitos no CPF: a máscara aplica pontos e traço sozinha. Teste 111.111.111-11 (rejeitado por repetição) e 123.456.789-00 (rejeitado pelos dígitos verificadores). Um CPF válido de verdade deixa a borda verde.
No campo de nascimento, digite 31/02/2000: "Essa data não existe no calendário." Digite uma data que resulte em menos de 16 anos: "É necessário ter ao menos 16 anos para se inscrever."
Clique em "Enviar inscrição" com o formulário vazio: todos os erros aparecem de uma vez, o foco vai para o primeiro campo inválido, a página rola até ele e o aviso do topo diz "Confira os campos destacados antes de enviar."
Preencha tudo corretamente e envie: o console mostra o objeto com cpf e telefonesem máscara, atividades como array, e a página exibe a mensagem verde de sucesso.
Preencha só o nome e o e-mail e recarregue a página (F5dronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele.
): os valores voltam e o aviso diz que o rascunho foi recuperado. Confira no DevTools → Application → Local Storage que a chave inscricao:rascunhonão contém CPF nem telefone. Clique em "Limpar rascunho" e recarregue: o formulário volta vazio.
Abra programacao.html: os doze cartões do dados.js aparecem ordenados por dia e horário, e o contador diz "12 atividades encontradas". Cada cartão mostra o nome de quem apresenta — resolvido pelo palestranteId, não digitado.
Digite seguranca (sem cedilha e sem acento) na busca: aparece "Segurança em aplicações web: dez erros comuns". Digite carla: aparecem as duas atividades de Carla Mendes, provando que a busca cruza as duas listas. Digite xyz: a mensagem de estado vazio explica o que fazer.
Mude "Ordenar por" para "Vagas restantes": o minicurso de Git e o de redes neurais (ambos esgotados) vão para o fim e ganham o selo "Esgotado". Mude para "Título (A–Z)" e confira que os acentos ficam na ordem correta. No filtro de área, "Segurança" deixa duas atividades e "Inteligência Artificial", outras duas.
Percorra a página inteira com Tabcê já fez
Tags: acessibilidade, formularios, investigacao, projeto
Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.
Critérios de pronto
Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.
: todo campo tem foco visível, o botão "Limpar filtros" devolve o foco à busca, e o console está sem nenhum erro em vermelho nas cinco páginas — em especial, nenhum ReferenceError: debounce is not defined e nenhum Identifier … has already been declared.
11. Ainda em inscricao.html, confira que a seção de vagas continua no topo, com o número calculado e o aviso "Últimas vagas!" das Aulas 10 e 11 — e que ele é visualmente diferente do aviso verde/vermelho do envio do formulário.
A1. Por que event.preventDefault() é obrigatório no ouvinte de submit quando o formulário é processado em JavaScript? O que acontece exatamente sem ele?
A2. Cite as três formas de ler os valores de um formulário e uma vantagem de cada uma.
A3. O que input.value devolve em um campo type="number"? Escreva a conversão correta e diga o que acontece se o campo estiver vazio.
A4. Diferencie os eventos input, change e blur para fins de validação. Qual deles não borbulha, e qual é a alternativa borbulhante?
A5. Para que serve o atributo novalidate no <form>? Por que mantemos required e pattern no HTML mesmo assim?
A6. O que fazem campo.checkValidity(), campo.reportValidity() e campo.setCustomValidity()? Qual armadilha o terceiro traz?
A7. Por que sinalizar erro apenas pela cor da borda é um problema de acessibilidade? Cite duas formas de resolver.
A8. Para que servem role="alert", aria-invalid e aria-describedby na exibição de erros? O que cada um muda para quem usa leitor de tela?
A9. Reescreva como mensagem útil: "Erro no campo 3". Invente o contexto que preferir e justifique a sua escolha em uma linha.
A10. Por que a validação no navegador não substitui a validação no servidor? Descreva, em duas frases, como alguém burlaria a sua validação.
A11. O que faz Object.fromEntries(new FormData(form).entries())? Cite duas limitações do FormData.
A12. Por que a máscara precisa ser removida antes de validar e de enviar o dado?
A13. O que fazem as flags g, i e m de uma expressão regular?
A14. Qual a diferença entre \d, \w e \s? E entre \d e \D?
A15. O que casa cada padrão: /a+/, /a*/, /a?/, /a{2,4}/? Dê um exemplo de string aceita e uma rejeitada para cada.
A16. Diferencie quantificador guloso de preguiçoso, com um exemplo próprio.
A17. Qual a diferença entre /\d{3}/ e /^\d{3}$/? Por que isso é crítico em validação?
A18. O que faz \b? Dê um exemplo em que a presença dele muda o resultado.
A19. Diferencie (abc) de (?:abc). Quando você usaria o segundo?
A20. O que faz o lookahead (?=…)? Escreva uma regex que aceite apenas strings que contenham ao menos um dígito, em qualquer posição.
A21. Escreva a regex para: (a) somente dígitos; (b) somente letras, com acento; (c) alfanumérico de 6 a 12 caracteres; (d) começa com maiúscula; (e) termina com .pdf.
A22. Diferencie test(), match() e matchAll(). Qual deles exige a flag g?
A23. Escreva o replace que converta "07/05/1998" em "1998-05-07", usando grupos capturados.
A24. Por que a validação de e-mail por regex não deve ser rigorosa demais? Qual é a estratégia profissional recomendada?
A25. O que a regex não consegue validar em um CPF? E em uma data?
A26. O que é ReDoS e qual padrão de escrita costuma causá-lo?
A27. Por que [...palestras] aparece antes do sort() em obterPalestrasVisiveis()? Descreva o bug que surge sem essa cópia.
A28. O que localeCompare(b, "pt-BR") resolve que o sort() puro não resolve? Dê um exemplo com nomes acentuados.
A29. Explique, linha a linha, o que a função normalizar() faz com a string "Segurança".
A30. Por que o <p> do contador de resultados precisa existir vazio no HTML em vez de ser criado pelo JavaScript?
B1. Implemente um formulário de login com validação de e-mail, senha de no mínimo 8 caracteres, botão "mostrar senha" que alterna o type do campo, bloqueio após três tentativas com contagem regressiva de 30 segundos e mensagens acessíveis por campo.
Resultado esperado: as três tentativas erradas desabilitam o botão de envio e mostram "Tente novamente em 30, 29, 28… segundos"; o botão "mostrar senha" tem aria-pressed sincronizado; nenhum erro é sinalizado só por cor.
Dica
Guarde o número de tentativas em uma variável de estado e use setInterval para a contagem, limpando com clearInterval ao chegar a zero. Para alternar o tipo do campo: campo.type = campo.type === "password" ? "text" : "password".
B2. Crie um medidor de força de senha com cinco critérios (comprimento ≥ 8, maiúscula, minúscula, número e caractere especial), exibindo a lista de critérios que ficam verdes conforme atendidos e uma barra de progresso colorida.
Resultado esperado: cada critério é um <li> com um ícone que muda de estado; a barra usa <meter> ou uma div com largura proporcional; o texto do nível ("Fraca", "Razoável", "Forte") é anunciado por uma região aria-live.
Dica
Uma regex por critério: /[A-Z]/, /[a-z]/, /\d/, /[^A-Za-z0-9]/. Some um ponto por critério atendido e penalize repetições com /(.)\1{2,}/. Nunca guarde a senha em localStorage.
B3. Implemente a validação de data de nascimento como um componente independente: o campo calcula a idade exata em tempo real e a exibe ao lado ("Você tem 19 anos"), rejeita datas futuras, datas que não existem no calendário e idade abaixo de 16 anos.
Resultado esperado: digitar 29/02/2001 mostra "Essa data não existe no calendário" (2001 não é bissexto); digitar 29/02/2000 é aceito; a idade aparece assim que a data fica completa e válida.
Dica
Reaproveite calcularIdade da Mão na massa e teste a existência da data reconstruindo dia, mês e ano do objeto Date, como explica a seção 4.1. Para o ano bissexto você não precisa de regra própria: o Date já sabe.
B4. Construa um formulário de agendamento de sala com: data (não pode ser passada nem fim de semana), horário em um <select> gerado dinamicamente de 30 em 30 minutos entre 8h e 18h, duração em horas e observações, exibindo um resumo textual do agendamento antes da confirmação.
Resultado esperado: as opções do <select> são criadas por um laço, não escritas à mão; escolher um sábado mostra "A sala não é liberada aos fins de semana"; o resumo é atualizado a cada alteração e lido por uma região aria-live.
Dica
data.getDay() devolve 0 para domingo e 6 para sábado. Para gerar os horários, um for de 8 a 17 com dois push por hora (:00 e :30) resolve. Use String(hora).padStart(2, "0") para formatar.
B5. Pegue o formulário de contato de contato.html (Unidade 1), que hoje só tem validação nativa, e adicione validação completa em JavaScript sem alterar o HTML além de acrescentar os <span> de erro. Documente cada regra implementada em um comentário no topo do arquivo.
Resultado esperado: um arquivo js/contato.js com validadores puros e reaproveitáveis; o HTML mantém required e type, e ganha apenas os <span class="erro" role="alert">; o console fica limpo.
Dica
Comece copiando a estrutura de js/inscricao.js e apagando o que não se aplica. As funções mostrarErro, validarEmail e validarNomeCompleto saem de lá sem alteração — esse é o ponto de escrever validadores puros.
B6. Escreva e teste expressões regulares para validar: matrícula acadêmica (quatro dígitos de ano seguidos de cinco dígitos), placa Mercosul, código de rastreio dos Correios (duas letras, nove dígitos e BR), IPv4 e cartão com 16 dígitos com ou sem espaços e hífens.
Resultado esperado: uma página exercicios/aula14/regex.html com um campo por padrão, validação em tempo real e, ao lado de cada campo, três exemplos válidos e três inválidos que você testou.
Dica
Ancore tudo com ^ e $. Para o IPv4, cada octeto vai de 0 a 255: (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d) repetido quatro vezes, separado por \.. Teste cada expressão no https://regex101.com antes de colar no código.
B7. Escreva gerarSlug(titulo) que converta "Minicurso: Acessibilidade na Prática — Nível 1!" em "minicurso-acessibilidade-na-pratica-nivel-1", tratando acentos, símbolos, espaços múltiplos e hífens nas pontas.
Resultado esperado: uma função pura com cinco casos de teste no console, incluindo uma string só com símbolos (que deve devolver string vazia) e uma com espaços nas pontas.
Dica
O caminho é: normalizar (seção 9.2) para tirar acentos, replace(/[^a-z0-9]+/g, "-") para trocar tudo que não é alfanumérico por hífen e replace(/^-+|-+$/g, "") para limpar as pontas.
B8. Implemente um destacador de busca: dado o termo digitado e o texto de cada cartão da programação, envolva todas as ocorrências em <mark>, ignorando maiúsculas e acentos, sem quebrar quando o termo contiver caracteres especiais de regex.
Resultado esperado: buscar web destaca "web" em todos os títulos; buscar ( não gera erro no console; o destaque some ao limpar a busca.
Dica
Escape o termo antes de montar a regex: termo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"). Como o destaque exige HTML, este é o único lugar da aula em que innerHTML é aceitável — e só porque o texto vem dos seus dados, não do usuário. Sanitize antes.
B9. Acrescente paginação à programação: quatro itens por página, botões "anterior" e "próxima", indicação "Página 2 de 3" e volta automática à página 1 sempre que a busca ou o filtro mudarem.
Resultado esperado: a paginação respeita o resultado filtrado (não o array completo); os botões ficam desabilitados nos extremos; o foco vai para o topo da lista ao trocar de página.
Dica
Guarde paginaAtual no estado e use lista.slice(inicio, inicio + POR_PAGINA). O total de páginas é Math.ceil(lista.length / POR_PAGINA) — e nunca deixe paginaAtual maior que ele depois de um filtro.
C1. Biblioteca de validação configurável. Escreva um js/validador.js genérico, dirigido por um objeto de regras, que funcione em qualquer formulário do seu projeto:
Deve suportar regras predefinidas por tipo, regras personalizadas por função, mensagens customizadas, validação em blur/input/submit, exibição automática das mensagens e um método validador.estaValido(). Demonstre a mesma biblioteca funcionando em três formulários diferentes do seu projeto.
Dica
criarValidador devolve um objeto com métodos — é o padrão de módulo que você reencontrará no Nível 2. Guarde as regras em uma constante e percorra Object.entries(regras) para registrar os ouvintes. Cada tipo é uma entrada em um objeto VALIDADORES_POR_TIPO, o que evita uma cadeia gigante de if.
C2. Painel de consulta completo. Sobre a listagem principal do seu projeto autoral, implemente ao mesmo tempo: busca com debounce e normalização de acentos, dois filtros combináveis (categoria e faixa de valor ou data), ordenação por três critérios, paginação, contagem de resultados anunciada, estado vazio com sugestão útil e persistência da consulta no localStorage (ao recarregar, os filtros voltam como estavam).
Dica
Mantenha um objeto de estado (const consulta = { termo: "", categoria: "todas", ordem: "nome", pagina: 1 }) e uma função aplicarConsulta() que lê esse objeto, filtra, ordena, pagina e renderiza. Salvar e restaurar vira JSON.stringify(consulta) e Object.assign(consulta, JSON.parse(bruto)).
O arquivo abaixo deveria validar uma inscrição, mas aceita nome vazio, e-mail sem arroba e CPF com uma letra no meio — e, quando reclama, ninguém que use leitor de tela fica sabendo. São seis defeitos: quatro de lógica e dois de acessibilidade. Encontre todos usando só o DevTools e o console, sem reescrever o arquivo do zero.
Um espaço em branco (" ") no campo nome passa a ser rejeitado.
"ana@teste" e "ana @teste.br" passam a ser rejeitados, e "ana@teste.br" continua sendo aceito.
O envio passa pelo evento submit do formulário e funciona com Enter em qualquer campo.
Cada mensagem é anunciada por leitor de tela e o campo com erro fica marcado com aria-invalid.
As mensagens dizem o que fazer, não apenas "Inválido".
Um comentário de uma linha acima de cada correção explica o defeito que ela resolve.
Pistas
Compare a regex de e-mail com a da seção 5.10: o que falta nas pontas dela? E o que \w deixa passar?
valor === "" e valor.trim() === "" não são a mesma pergunta.
Releia a Aula 13 sobre por que ouvir click no botão não é o mesmo que ouvir submit no formulário.
innerHTML para texto puro é desnecessário e arriscado; e cor não é mensagem. Volte à seção 7 e liste os cinco requisitos.
⭐⭐
⭐⭐ Busca que ignora acentos, plurais e a ordem das palavras¶
javascriptinvestigacaoperformancedom
Digite "minicurso git" na busca da programação: nada aparece, porque nenhum título contém exatamente essa sequência. Digite "seguranças": também nada. Usuários não digitam do jeito que os seus dados foram escritos — e a sua busca não pode depender disso. Torne a busca tolerante: ela deve encontrar o item quando todas as palavras digitadas aparecerem no texto, em qualquer ordem, ignorando acentos, maiúsculas e o "s" final.
Critérios de pronto
"minicurso git" encontra "Minicurso: Git e GitHub do zero ao primeiro pull request".
"git minicurso" encontra o mesmo item (a ordem das palavras não importa).
"seguranças" encontra "segurança em aplicações web".
A tolerância está isolada em uma função pura, testada no console com pelo menos seis casos.
A busca continua respondendo instantaneamente com 500 itens no array (meça com console.time antes e depois).
Pistas
Divida o termo digitado em palavras com split(/\s+/) e exija que todas passem: palavras.every(...).
String.prototype.normalize("NFD") mais a faixa ̀-ͯ resolvem os acentos; a seção 9.2 tem a receita.
Para o plural ingênuo, remova um s final de cada palavra antes de comparar — e documente em um comentário que essa é uma simplificação, não uma regra do português.
Normalizar o texto de cada item a cada tecla é desperdício. Faça isso uma vez, guardando o texto já normalizado em um campo do próprio objeto ao iniciar.
O validarCampo da Mão na massa tem um switch com sete casos. Funciona, mas cada campo novo exige mexer nessa função — e, num formulário de vinte campos, ela vira um monstro. Existe uma alternativa que os frameworks usam: descrever as regras como dados e deixar uma função genérica aplicá-las. Refatore a validação da inscrição para esse modelo, sem perder nenhuma mensagem nem nenhum comportamento.
Critérios de pronto
Existe um objeto REGRAS mapeando o id de cada campo para uma lista de validadores.
validarCampo(campo) tem no máximo dez linhas e não cita nenhum campo pelo nome.
Acrescentar um campo novo ao formulário exige alterar apenas o HTML e o objeto REGRAS.
Todos os comportamentos do Como testar continuam funcionando, item por item.
Um comentário no topo explica, em três linhas, a vantagem dessa organização.
Pistas
Um validador é uma função (valor) => mensagem. Uma lista deles pode ser percorrida com find para achar a primeira mensagem não vazia.
Regras que precisam de parâmetro (tamanho mínimo, idade mínima) viram funções que devolvem funções: minimo(5) devolve (valor) => ....
O campo termos trabalha com checked, não com value. Padronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele.
Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.
Critérios de pronto
Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.
Pistas
Comece navegando só com Tab, Shift+Tab, Espaço e Enter. Anote todo momento em que você não soube onde estava.
O resumo de erros é uma <div tabindex="-1"> com uma lista de links href="#id-do-campo"; ao falhar o envio, chame .focus() nela.
Ferramentas automáticas não detectam rótulo errado, ordem de foco ilógica nem mensagem que não diz o que fazer. É aí que estão os seus dois achados.
O leitor de tela do seu sistema já está instalado. No Windows, o Narrador abre com Ctrl+Win+Enter; no Linux, o Orca com Super+Alt+S.
Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulo de expressões regulares. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, seção de validação de dados de entrada. Na MDN em português, o artigo "Validação de formulário no lado do cliente" (link em Para aprofundar). Anote duas restrições de validação nativa que não apareceram nesta aula.
Parte 2 — Entrega (40 min). No seu projeto autoral:
O formulário principal com validação completa em JavaScript: pelo menos seis campos, mensagens específicas por campo, ao menos uma validação por expressão regular e uma regra de negócio que a regex não resolve (dígito verificador, idade mínima, data no calendário ou confirmação de senha).
Mensagens acessíveis: role="alert", aria-invalid, aria-describedby, indicação que não dependa só de cor e foco no primeiro campo inválido ao enviar.
A listagem principal do seu domínio com busca (com debounce e normalização de acentos), um filtro e uma ordenação, com contador anunciado e estado vazio tratado.
Os exercícios B2 (medidor de força de senha) e B7 (gerarSlug) em exercicios/aula14/.
Critério de pronto: enviar o formulário vazio destaca todos os campos com erro, leva o foco ao primeiro e não recarrega a página; digitar um termo sem acento encontra o item acentuado; o console fica limpo em todas as páginas.
Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga uma expressão regular sua que falhou em um caso que você não tinha previsto, explique por que falhou e mostre a correção.
Guarde no seu repositório: commit + push (ou a pasta do projeto).
web.dev — Learn Forms: https://web.dev/learn/forms — os módulos de validação e de acessibilidade em formulários, com exemplos interativos.
regex101: https://regex101.com — testa a expressão, explica token por token e mede o desempenho. Escolha o sabor ECMAScript (JavaScript).
Regexr: https://regexr.com — alternativa com uma biblioteca de padrões prontos para comparar com os seus.
FLANAGAN, David. JavaScript: o guia definitivo. Bookman, 2014 — capítulo de expressões regulares e de trabalho com formulários.
STEFANOV, Stoyan. Padrões JavaScript. Novatec, 2010 — o padrão de módulo, base do exercício C1.
MILETTO, Evandro M.; BERTAGNOLLI, Silvia C. Desenvolvimento de software II. Bookman, 2014 — validação de dados de entrada e tratamento de erros.
TERUEL, Evandro C. HTML 5 — Guia Prático. Saraiva, 2014 — capítulo de formulários, para revisar os atributos que alimentam a Constraint Validation API.
Na próxima aula, o site sai da sua máquina e vai para a internet: você vai preparar o projeto para produção, versioná-lo com Git, publicá-lo no GitHub Pages, auditar o resultado com o Lighthouse e fechar o Marco 3 — o site do evento, no ar, com um endereço que qualquer pessoa pode acessar.