⚡ Capítulo 20: Projeto Capstone JS Vanilla — Aplicação SPA Interativa com DOM, LocalStorage e Fetch API
🎯 Objetivos da Aula
Ao final desta aula, você será capaz de:
- Integrar todos os conceitos da Fase 04 (JS ES6+, Seletores DOM, Manipulação de Nós, Eventos,
async/await, Fetch API e LocalStorage). - Construir um Gerenciador Interativo de Cadastro de Alunos 100% em cliente (Single Page Application — SPA).
- Persistir e recuperar o estado da aplicação no navegador via
localStorage.getItemelocalStorage.setItem. - Consumir dados assíncronos de uma API simulada usando
fetch()com tratamento de errostry/catch.
🧠 1. Arquitetura da Aplicação Interativa em JS
A aplicação conecta a interface HTML com o estado dinâmico mantido pelo JavaScript na memória do navegador:
flowchart TD subgraph UI ["Interface HTML (DOM)"] Form["<form id='form-aluno'>"] Table["<table id='tabela-alunos'>"] BtnAPI["<button id='btn-carregar-api'>"] end subgraph AppLogic ["Lógica JavaScript (app.js)"] State["Vetor de Estado: let alunos = []"] EventHandlers["Tratadores de Evento (addEventListener)"] StorageHandler["Sincronização LocalStorage (JSON.stringify)"] APIHandler["Consumo Assíncrono (fetch + async/await)"] end Form -->|"submit"| EventHandlers EventHandlers --> State State --> StorageHandler & Table BtnAPI -->|"click"| APIHandler --> State
💻 2. Código Completo do Projeto Integrado (app.js)
Let’s write the complete, clean JavaScript code for the Capstone project:
// =================================================================
// PROJETO CAPSTONE FASE 04: SISTEMA INTERATIVO DE GESTÃO DE ALUNOS
// =================================================================
// 1. ESTADO DA APLICAÇÃO (Carregado do LocalStorage)
let listaAlunos = JSON.parse(localStorage.getItem("@portal:alunos")) || [];
// 2. SELEÇÃO DOS ELEMENTOS DO DOM
const formAluno = document.getElementById("form-aluno");
const inputMatricula = document.getElementById("input-matricula");
const inputNome = document.getElementById("input-nome");
const inputCurso = document.getElementById("input-curso");
const tabelaCorpo = document.getElementById("tabela-corpo-alunos");
const btnCarregarDadosAPI = document.getElementById("btn-carregar-api");
const mensagemFeedback = document.getElementById("mensagem-feedback");
// 3. INICIALIZAÇÃO DA APLICAÇÃO
document.addEventListener("DOMContentLoaded", () => {
console.log(">> Aplicação SPA carregada. Registros salvos:", listaAlunos.length);
renderizarTabela();
});
// 4. CADASTRO DE NOVO ALUNO (Evento Submit)
formAluno.addEventListener("submit", (evento) => {
evento.preventDefault(); // Impede o recarregamento da página
const matricula = inputMatricula.value.trim();
const nome = inputNome.value.trim();
const curso = inputCurso.value.trim();
if (!matricula || !nome || !curso) {
exibirFeedback("Preencha todos os campos obrigatórios!", "erro");
return;
}
// Criando novo objeto de aluno
const novoAluno = {
id: Date.now(),
matricula: matricula,
nome: nome,
curso: curso,
dataCadastro: new Date().toLocaleDateString("pt-BR")
};
listaAlunos.push(novoAluno);
salvarEstado();
renderizarTabela();
formAluno.reset();
exibirFeedback("Aluno cadastrado com sucesso!", "sucesso");
});
// 5. RENDERIZAÇÃO DA TABELA NO DOM
function renderizarTabela() {
tabelaCorpo.innerHTML = ""; // Limpa a tabela antes de renderizar
if (listaAlunos.length === 0) {
tabelaCorpo.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; color: #777;">
Nenhum aluno cadastrado no momento.
</td>
</tr>
`;
return;
}
listaAlunos.forEach((aluno) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${aluno.matricula}</td>
<td><strong>${aluno.nome}</strong></td>
<td>${aluno.curso}</td>
<td>${aluno.dataCadastro}</td>
<td>
<button class="btn-excluir" onclick="removerAluno(${aluno.id})">❌ Excluir</button>
</td>
`;
tabelaCorpo.appendChild(tr);
});
}
// 6. REMOÇÃO DE ALUNO DO ESTADO
window.removerAluno = function(id) {
listaAlunos = listaAlunos.filter(aluno => aluno.id !== id);
salvarEstado();
renderizarTabela();
exibirFeedback("Registro de aluno removido com sucesso.", "alerta");
};
// 7. PERSISTÊNCIA NO LOCALSTORAGE
function salvarEstado() {
localStorage.setItem("@portal:alunos", JSON.stringify(listaAlunos));
}
// 8. CONSUMO ASSÍNCRONO DE API COM FETCH & ASYNC/AWAIT
btnCarregarDadosAPI.addEventListener("click", async () => {
exibirFeedback("Carregando dados da API externa...", "info");
try {
// Simulando requisicao a API REST externa (JSONPlaceholder / API de Testes)
const resposta = await fetch("https://jsonplaceholder.typicode.com/users?_limit=3");
if (!resposta.ok) {
throw new Error(`Erro HTTP: status ${resposta.status}`);
}
const usuariosAPI = await resposta.json();
// Mapeando dados da API para o formato do nosso sistema
usuariosAPI.forEach(usuario => {
listaAlunos.push({
id: Date.now() + Math.random(),
matricula: `2026${usuario.id}`,
nome: usuario.name,
curso: "Engenharia de Software (Importado)",
dataCadastro: new Date().toLocaleDateString("pt-BR")
});
});
salvarEstado();
renderizarTabela();
exibirFeedback("3 alunos importados da API com sucesso!", "sucesso");
} catch (erro) {
console.error(">> Falha na requisição da API:", erro);
exibirFeedback("Falha ao conectar com a API externa.", "erro");
}
});
// 9. FUNÇÃO DE FEEDBACK VISUAL NA TELA
function exibirFeedback(mensagem, tipo) {
mensagemFeedback.innerText = mensagem;
mensagemFeedback.className = `feedback feedback-${tipo}`;
mensagemFeedback.style.display = "block";
setTimeout(() => {
mensagemFeedback.style.display = "none";
}, 4000);
}⚔️ 3. Desafios Práticos (Exercícios 30/50/20)
🥉 Nível Bronze (Fixação)
- Para que serve o método
JSON.stringify()ao salvar dados nolocalStorage? E por que utilizamosJSON.parse()ao carregar os dados salvos? - O que faz o método
evento.preventDefault()dentro de um escutador de evento de envio de formulário (submit)?
🥈 Nível Prata (Aplicação)
- Adicione ao projeto a funcionalidade de busca/filtro por nome em tempo real (evento
inputno campo de busca), renderizando apenas os alunos cujo nome contenha o termo digitado pelo usuário.
🥇 Nível Ouro (Desafio)
- Implemente um botão “Limpar Todos os Registros” no sistema, solicitando confirmação via
confirm()do JavaScript antes de executarlocalStorage.clear(), zerar o vetor de estado e atualizar a tabela.
💡 Gabaritos Sanfonados de Resposta
💡 Ver Gabarito do Nível Bronze
- O
localStorageaceita apenas textos (Strings). OJSON.stringify()converte objetos e arrays JavaScript em String formatada. OJSON.parse()reconverte essa string de volta para objetos e vetores operáveis no JS. - Evita o comportamento padrão dos navegadores de recarregar a página e tentar enviar uma requisição HTTP síncrona ao submeter um
<form>, permitindo o processamento 100% em cliente (SPA).
💡 Ver Gabarito do Nível Prata & Ouro (Código JS)
// Gabarito Nível Ouro: Botão Limpar Tudo
const btnLimparTudo = document.getElementById("btn-limpar-tudo");
btnLimparTudo.addEventListener("click", () => {
const confirmacao = confirm("Tem certeza que deseja apagar TODOS os registros de alunos?");
if (confirmacao) {
listaAlunos = [];
localStorage.removeItem("@portal:alunos");
renderizarTabela();
exibirFeedback("Todos os registros foram apagados.", "alerta");
}
});📚 Referências Teóricas Oficiais
- ECMA INTERNATIONAL. ECMAScript 2023 Specification — Async/Await & Promises.
- WHATWG HTML SPECIFICATION. Web Storage API — LocalStorage & SessionStorage.
⬅️ Capítulo Anterior (Boas Práticas de JS) | Voltar ao Sumário da Fase 04 | Avançar para a Fase 05 (TypeScript) ➡️