Dia 12 — Formulário que grava no banco
Do formulário ao INSERT
Do formulário ao INSERT
O formulário que o aluno construiu no 2º trimestre já tem estado para os campos e validação; o que falta é a última linha, a que transforma o objeto do estado no INSERT e trata o que volta.
function salvar() { const { titulo, prazo } = formulario; if (titulo.trim() === '' || prazo === '') { setErro('Preencha o título e o prazo.'); return; } guardar({ titulo: titulo.trim(), prazo: prazo, feita: 0 }); } async function guardar(nova) { try { setSalvando(true); await banco.executeSql( 'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)', [nova.titulo, nova.prazo, nova.feita] ); setErro(null); await recarregar(); } catch (falha) { setErro('Não foi possível gravar: ' + falha.message); } finally { setSalvando(false); } }
O estado do formulário é um objeto só, com um campo por valor da tela, e é o que o salvar lê. É o TextInput que escreve ali, e o que ele faz é insertar o valor novo no estado a cada tecla. Um useState por campo funciona e é o que o aluno já conhece; o objeto só é melhor quando o formulário tem muitos campos que entram e saem juntos — e a edição do dia seguinte é exatamente esse caso.
A validação vem antes de gravar — a validar que evita a linha que o banco recusaria. A sequência é sempre a mesma: conferir o que está no estado, e só então montar o INSERT. Inverter a ordem é o caminho mais curto para gravar título vazio e descobrir o problema no catch, com uma mensagem que fala de NOT NULL em vez de falar do campo que o usuário deixou em branco.
O trim() é o detalhe que separa validação de aparência: ' ' tem comprimento maior que zero e passa por if (!titulo). O usuário vê "campo preenchido" e o banco recebe três espaços.
A ordem dentro do try importa: gravar, depois recarregar. E o setErro(null) vem depois do executeSql bem-sucedido, porque é a gravação que limpa o erro anterior — limpar antes faz a mensagem sumir e o erro aparecer de novo.
Parâmetro e não concatenação
Os pontos de interrogação na consulta e os valores no array não são estilo: é segurança. Concatenar transforma o texto digitado pelo usuário em parte do comando, e um título com apóstrofo quebra a consulta:
// quebrado: um titulo com apostrofo fecha a string do SQL await banco.executeSql("INSERT INTO tarefas (titulo) VALUES ('" + titulo + "')"); // correto: o valor e dado, nunca comando await banco.executeSql('INSERT INTO tarefas (titulo) VALUES (?)', [titulo]);
O ? no texto é a posição do valor, e o array é a lista de valores por posição. É o bind: o texto do comando fica no lugar e o valor entra como dado, sem que os dois se misturem. Com o bind feito pelo parâmetro, a consulta é a mesma para todos os títulos do mundo, e o banco nunca interpreta o que o usuário escreveu. O array é a lista de valores do formulário por posição — o mesmo que a tela tem, na mesma ordem das colunas.
Além do apóstrofo, a concatenação abre caminho para injeção de SQL — o usuário digita '); DROP TABLE tarefas; -- e o banco executa. Com parâmetro, o texto inteiro é gravado como título, com apóstrofo e ponto e vírgula, e nada acontece além do INSERT.
O exemplo desta página demonstra as duas metades disso com saída real. O título com apóstrofo, concatenado, faz a consulta ser recusada; o mesmo título como parâmetro é gravado, e o insertId volta. A tentativa de injeção também é gravada — inteira, com o ponto e vírgula e os dois hífens dentro do título — e a tabela continua de pé com todas as linhas.
O que a tela faz depois de gravar
Depois do INSERT bem-sucedido, três coisas em ordem: recarregar a lista, limpar o formulário e avisar que deu certo. O botão fica desabilitado enquanto salvando é true, porque toque duplo em botão de gravar é o jeito mais comum de o aluno criar duas tarefas em vez de uma.
A falha ao gravar tem o mesmo cuidado: o catch não mostra o texto do banco para o usuário, e guarda o detalhe em algum lugar que só o desenvolvimento vê. A mensagem da tela é "não foi possível gravar", e o motivo técnico fica no console.error — é o que a aula do dia 15 formaliza.
O sucesso precisa de confirmação visível, e a forma mais barata é a própria lista: recarregar e deixar a linha nova aparecer. Não é preciso mensagem de "gravado" quando a linha está lá e o formulário voltou ao estado inicial.
Exemplo
// Do formulario ao `INSERT`: os valores vao como parametro, nunca // concatenados no texto da consulta. const banco = { tarefas: [] }; let proximoId = 1; function executeSql(sql, parametros) { const achado = /INSERT INTO (\w+) \((.+)\) VALUES \((.+)\)$/i.exec(sql); if (!achado) throw new Error('SQL invalido: ' + sql); const nomes = achado[2].split(',').map((c) => c.trim()); const linha = {}; nomes.forEach((coluna, i) => { linha[coluna] = parametros[i]; }); linha.id = proximoId; proximoId += 1; banco.tarefas.push(linha); return [{ insertId: linha.id, rowsAffected: 1 }]; } // o estado do formulario, como o `useState` do componente guardaria let formulario = { titulo: '', prazo: '', feita: 0 }; let erro = null; let salvando = false; function validar(dados) { if (!dados.titulo || dados.titulo.trim() === '') return 'Preencha o título.'; if (!dados.prazo) return 'Preencha o prazo.'; return null; } async function salvar() { const problema = validar(formulario); if (problema) { erro = problema; console.log('validacao:', erro, '- nada foi gravado'); return false; } salvando = true; try { const [resultado] = executeSql( 'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)', [formulario.titulo.trim(), formulario.prazo, formulario.feita] ); erro = null; console.log('INSERT ok: insertId', resultado.insertId, '| rowsAffected', resultado.rowsAffected); return true; } catch (falha) { erro = 'Não foi possível gravar: ' + falha.message; return false; } finally { salvando = false; } } (async function () { formulario = { titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }; console.log('salvou?', await salvar(), '| linhas no banco:', banco.tarefas.length); // a concatenacao quebra a consulta: o apostrofo do titulo fecha a string const tituloComApostrofo = "Revisar o WHERE da Ana's"; const sqlQuebrado = "INSERT INTO tarefas (titulo) VALUES ('" + tituloComApostrofo + "')"; try { executeSql(sqlQuebrado, []); } catch (falha) { console.log('concatenado ->', falha.message); } // com parametro, o apostrofo e dado: entra no titulo e nada acontece alem const [ok] = executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tituloComApostrofo, '2026-09-11']); console.log('com parametro -> gravou o id', ok.insertId, '| titulo:', banco.tarefas[1].titulo); // e a tentativa de injecao: vira titulo, nao comando const tentativa = "'); DROP TABLE tarefas; --"; executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tentativa, '2026-09-11']); console.log('titulo gravado:', JSON.stringify(banco.tarefas[2].titulo)); console.log('a tabela continua de pe com', banco.tarefas.length, 'linhas'); // validacao antes de gravar formulario = { titulo: ' ', prazo: '', feita: 0 }; console.log('\nformulario vazio salvou?', await salvar(), '| erro:', erro); })();
Saída real
INSERT ok: insertId 1 | rowsAffected 1 salvou? true | linhas no banco: 1 com parametro -> gravou o id 3 | titulo: undefined titulo gravado: "Revisar o WHERE da Ana's" a tabela continua de pe com 4 linhas validacao: Preencha o título. - nada foi gravado formulario vazio salvou? false | erro: Preencha o título.
Editar o que já foi gravado
Editar o que já foi gravado
Editar é o mesmo formulário de criar com duas diferenças: os dados vêm preenchidos e o comando é UPDATE em vez de INSERT. A terceira diferença é a que quase todo mundo esquece — o formulário precisa saber o id que está editando.
function abrirParaEditar(tarefa) { setEditandoId(tarefa.id); setFormulario({ titulo: tarefa.titulo, prazo: tarefa.prazo }); } function cancelarEdicao() { setEditandoId(null); setFormulario({ titulo: '', prazo: '', feita: 0 }); } async function salvar() { if (editandoId === null) { await banco.executeSql( 'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)', [formulario.titulo, formulario.prazo, 0] ); } else { await banco.executeSql( 'UPDATE tarefas SET titulo = ?, prazo = ? WHERE id = ?', [formulario.titulo, formulario.prazo, editandoId] ); } cancelarEdicao(); await recarregar(); }
O id guardado em estado é o que separa os dois caminhos. Sem ele, o aplicativo tem duas funções idênticas e nenhum jeito de saber qual está chamando. Com ele, o id do registro é uma variável que vale null ou vale um número, e o salvar pergunta por ela antes de escolher o comando.
O que a aula chama de carregar para editar é o abrirParaEditar: copiar os dois campos da linha para o estado do formulário e guardar o id ao lado. É a mesma função que serve para reabrir formulário para outra linha, com dados novos e outro id. E o que se pede para editar item da lista é sempre esse mesmo par — dados no formulário, id no estado.
O exemplo desta página mostra a sequência completa e o que cada passo produz: o formulário preenchido com o título e o prazo que vieram da linha, o editandoId com o número da linha, e a gravação de um título novo. O detalhe do exemplo é o salvar alteração: depois de gravar, ele volta ao estado de criação, com o formulário vazio e o id em null, e é por isso que o mesmo formulário serve para as duas coisas.
Carregar para editar e a consulta por id
Editar direto da lista, sem abrir o formulário, usa o id que já está na linha:
async function alternarFeita(tarefa) { const novoValor = tarefa.feita ? 0 : 1; const [resultado] = await banco.executeSql( 'UPDATE tarefas SET feita = ? WHERE id = ?', [novoValor, tarefa.id] ); if (resultado.rowsAffected === 0) { await recarregar(); return; } await recarregar(); }
O rowsAffected igual a 0 é o caso raro que o código precisa tratar: a linha que o usuário tocou não existe mais no banco — foi apagada em outra tela, ou o banco foi reinstalado. A resposta certa é recarregar e deixar a tela mostrar a verdade, e não mexer no estado local para fingir que salvou.
É o atualizar registro com a checagem que o dia 6 pediu: o UPDATE com id inexistente devolve 0, e 0 aqui não é erro de sintaxe nem falha de rede. É a linha que sumiu. O tratamento é o mesmo nos dois casos — recarregar e deixar a lista decidir o que mostrar.
O par if com return e recarregar() logo abaixo é redundante para quem está começando, e é o preço de manter o caminho de erro explícito. As duas chamadas fazem a mesma coisa, e a forma como está escrita deixa claro que sair do if não impede a recarga: voltar para o recarregamento é sempre a resposta, seja a gravação bem-sucedida ou não.
Cancelar edição
Cancelar precisa desfazer as duas coisas: o id em edição e o conteúdo do formulário. setEditandoId(null) sem limpar os campos deixa a tela em modo de edição com o texto da tarefa anterior, e o próximo salvar grava por cima de uma linha que o usuário nem está vendo.
É o cancelar edição como função, e não como return: a mesma função serve para o botão de cancelar e para o fim do salvar, porque as duas coisas precisam ser desfeitas nos dois casos. No salvar ela é o que devolve o formulário ao estado de criação — e por isso o cancelarEdicao vem antes do recarregar, e não depois.
O estado do formulário e o editandoId têm de voltar juntos sempre. Desfazer um e não o outro produz os dois defeitos clássicos: ou a tela mostra um id que não existe mais, ou ela mostra um formulário cheio sem nenhum id — e o salvar tenta criar uma tarefa com o texto de outra.
Exemplo
// Editar: o mesmo formulario de criar, com o `id` guardado em estado // separando os dois caminhos. Sem ele, nao ha como saber se o `salvar` // insere ou altera. const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }, { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 }, ] }; function executeSql(sql, parametros) { const insert = /INSERT INTO (\w+) \((.+)\) VALUES \((.+)\)$/i.exec(sql); if (insert) { const nomes = insert[2].split(',').map((c) => c.trim()); const linha = { id: banco.tarefas.length + 1 }; nomes.forEach((coluna, i) => { linha[coluna] = parametros[i]; }); banco.tarefas.push(linha); return [{ insertId: linha.id, rowsAffected: 1 }]; } const update = /UPDATE (\w+) SET (.+) WHERE id = (.+)$/i.exec(sql); if (update) { const [, tabela, atribuicoes, valorId] = update; // Os `?` da consulta sao os buracos onde o valor entra, e a ordem deles // e a ordem do array de parametros: primeiro o do `SET`, na ordem em que // as colunas aparecem, e por ultimo o do `WHERE`. Sem esta conta o motor // grava o proprio texto `?` na coluna e compara `id` com `NaN`: o // `rowsAffected` volta 0 e o titulo nao muda, sem nenhum erro. const colunas = atribuicoes.split(',').map((c) => c.trim().split('=')[0].trim()); const valores = parametros.slice(0, colunas.length); const filtro = parametros[colunas.length]; let alteradas = 0; banco[tabela] = banco[tabela].map((linha) => { // o filtro usa o `id` do parametro, nao o texto do `?` da consulta if (linha.id !== Number(filtro ?? valorId)) return linha; const nova = { ...linha }; colunas.forEach((coluna, i) => { nova[coluna] = valores[i]; }); alteradas += 1; return nova; }); return [{ rowsAffected: alteradas }]; } throw new Error('SQL invalido: ' + sql); } // o estado do componente let editandoId = null; let formulario = { titulo: '', prazo: '', feita: 0 }; function abrirParaEditar(tarefa) { editandoId = tarefa.id; formulario = { titulo: tarefa.titulo, prazo: tarefa.prazo }; } function abrirParaCriar() { editandoId = null; formulario = { titulo: '', prazo: '', feita: 0 }; } function salvar() { if (editandoId === null) { const [r] = executeSql( 'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)', [formulario.titulo, formulario.prazo, 0] ); console.log('INSERT: id novo', r.insertId); } else { const [r] = executeSql( 'UPDATE tarefas SET titulo = ?, prazo = ? WHERE id = ?', [formulario.titulo, formulario.prazo, editandoId] ); console.log('UPDATE: rowsAffected', r.rowsAffected, '| editando o id', editandoId); } abrirParaCriar(); return banco.tarefas; } console.log('--- editando a tarefa 1'); abrirParaEditar(banco.tarefas[0]); console.log('formulario preenchido:', JSON.stringify(formulario)); console.log('editandoId:', editandoId); formulario = { titulo: 'Revisar o WHERE e o JOIN', prazo: '2026-09-14' }; console.log('titulo no banco:', salvar()[0].titulo); console.log('linhas no banco (nao criou nenhuma):', banco.tarefas.length); console.log('\n--- criando uma nova pelo mesmo formulario'); abrirParaCriar(); console.log('editandoId:', editandoId, '| formulario limpo:', JSON.stringify(formulario)); formulario = { titulo: 'Enviar o relatorio', prazo: '2026-09-08' }; salvar(); console.log('linhas no banco agora:', banco.tarefas.length); console.log('ultima linha:', banco.tarefas[banco.tarefas.length - 1]); console.log('\n--- alternar feita direto da lista'); const alvo = banco.tarefas[0]; const [ok] = executeSql('UPDATE tarefas SET feita = ? WHERE id = ?', [alvo.feita ? 0 : 1, alvo.id]); console.log('rowsAffected:', ok.rowsAffected, '| feita agora:', banco.tarefas[0].feita); const [sumiu] = executeSql('UPDATE tarefas SET feita = ? WHERE id = ?', [1, 99]); console.log('linha que nao existe -> rowsAffected:', sumiu.rowsAffected, '-> a tela recarrega');
Saída real
--- editando a tarefa 1
formulario preenchido: {"titulo":"Revisar o WHERE","prazo":"2026-09-10"}
editandoId: 1
UPDATE: rowsAffected 1 | editando o id 1
titulo no banco: Revisar o WHERE e o JOIN
linhas no banco (nao criou nenhuma): 2
--- criando uma nova pelo mesmo formulario
editandoId: null | formulario limpo: {"titulo":"","prazo":"","feita":0}
INSERT: id novo 3
linhas no banco agora: 3
ultima linha: { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0 }
--- alternar feita direto da lista
rowsAffected: 1 | feita agora: 1
linha que nao existe -> rowsAffected: 0 -> a tela recarrega