Chame event.preventDefault() antes do primeiro await, execute a validação assíncrona e, em seguida, use form.requestSubmit() para iniciar uma nova tentativa de envio. Uma proteção de uso único permite que essa segunda tentativa prossiga sem repetir a verificação assíncrona.
Este tutorial é para quem está editando uma pequena interface de navegador em um Mac. Você criará um exemplo local em HTML no qual um formulário fecha um diálogo somente depois que uma verificação simulada do rótulo é aprovada. O mesmo padrão de tratamento de eventos se aplica a formulários comuns que navegam ou enviam uma solicitação.
Por que um manipulador assíncrono de envio não adia o envio?
O navegador não aguarda a promessa retornada por um ouvinte de evento. O algoritmo de despacho de eventos do DOM invoca os ouvintes e continua; o algoritmo de envio de formulários do HTML verifica se o evento submit foi cancelado antes de prosseguir. Adicionar async ao ouvinte não introduz uma etapa de espera nesse ciclo de vida. Padrão DOM: despacho de eventos, Padrão HTML: algoritmo de envio de formulários
Uma função assíncrona é executada de forma síncrona até seu primeiro await e, então, continua de forma assíncrona. Portanto, o cancelamento deve ocorrer antes do primeiro await, mesmo quando a promessa aguardada já foi cumprida. Chamar preventDefault() depois disso não pode desfazer uma decisão de envio que o navegador já tomou. MDN: função async
O cancelamento encerra a tentativa original; ele não a suspende para retomá-la depois. Quando a validação for bem-sucedida, seu código deverá solicitar explicitamente outro envio. Além disso, preventDefault() mantém a propagação do evento, então outros ouvintes ainda podem observá-lo. MDN: Event.preventDefault()
Escute o evento submit do formulário. Isso abrange o envio por botão, o envio implícito compatível, como ao pressionar Enter, e chamadas a requestSubmit(). Um manipulador de clique que só atua no botão não cobre todos os meios de envio. Normalmente, a validação nativa de restrições ocorre primeiro: um campo obrigatório inválido impede o envio sem disparar submit. MDN: evento submit de HTMLFormElement
Qual método preserva o comportamento de envio do formulário?
Use requestSubmit() quando a verificação bem-sucedida deve reiniciar o ciclo de vida normal do formulário. Chamar submit() diretamente ignora tanto a validação de restrições quanto o evento submit. Esse atalho altera o comportamento que seu manipulador assíncrono deve preservar. MDN: HTMLFormElement.submit()
| Abordagem | O que acontece | Adequação neste caso |
|---|---|---|
| Retornar uma promessa do ouvinte | O despacho do evento continua sem aguardá-la | Não adia a ação padrão |
Cancelar e, em seguida, chamar submit() |
O envio ignora a validação nativa e os ouvintes de submit | Altera o ciclo de vida |
Cancelar e, em seguida, chamar requestSubmit(submitter) |
A validação nativa e o evento submit são executados novamente | Preserva o fluxo de envio pretendido |
Capture event.submitter antes de aguardar. Passar esse botão para requestSubmit() preserva sua função, incluindo substituições de envio e sua contribuição de nome/valor, quando aplicável. Se não houver um botão de envio, chame requestSubmit() sem argumento. O botão fornecido ainda deve pertencer ao formulário; um botão inválido ou pertencente a outro formulário causa uma exceção. MDN: HTMLFormElement.requestSubmit()
O novo envio aciona seu ouvinte novamente. Portanto, o exemplo abaixo usa duas variáveis de estado separadas: pending impede verificações assíncronas simultâneas, enquanto allowNextSubmit autoriza exatamente uma passagem síncrona pelo manipulador. Uma flag permanente de “validado” seria abrangente demais, pois edições posteriores poderiam reutilizar um resultado antigo.
Crie um exemplo local completo no seu Mac
Salve o documento completo a seguir como async-form.html em um editor de código de texto simples e, em seguida, abra-o em um navegador atualizado no seu Mac. Não são necessárias bibliotecas, importações, servidor nem instalação. O padrão oferece suporte a requestSubmit() a partir do Safari 16. Padrão HTML: APIs de formulários
O exemplo usa method="dialog" para facilitar a visualização da ação padrão localmente. O envio bem-sucedido fecha o diálogo que contém o formulário e define seu returnValue com base no botão de envio; os controles do formulário não são transmitidos. O botão de abertura chama showModal() para exibi-lo. MDN: elemento dialog, MDN: HTMLDialogElement.showModal()
O validador é intencionalmente simulado: após um breve temporizador, aceita rótulos com pelo menos três caracteres após remover espaços em branco, rejeita o rótulo exatamente em minúsculas taken e lança uma exceção para o rótulo exatamente em minúsculas error. O atraso ilustra a conclusão assíncrona; não é uma medição de desempenho.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Asynchronous form validation</title>
</head>
<body>
<h1>Label preview</h1>
<button id="open" type="button">Open form</button>
<p id="outcome" role="status"></p>
<dialog id="editor" aria-labelledby="dialog-title">
<form id="label-form" method="dialog">
<h2 id="dialog-title">Choose a label</h2>
<label for="label">Label</label>
<input id="label" name="label" required autofocus>
<button type="submit" value="accepted">Continue</button>
<button id="cancel" type="button">Cancel</button>
<p id="status" role="status"></p>
</form>
</dialog>
<script>
const dialog = document.getElementById("editor");
const form = document.getElementById("label-form");
const input = document.getElementById("label");
const status = document.getElementById("status");
const outcome = document.getElementById("outcome");
const openButton = document.getElementById("open");
const cancelButton = document.getElementById("cancel");
let pending = false;
let allowNextSubmit = false;
let revision = 0;
async function validateLabel(value) {
await new Promise(resolve => setTimeout(resolve, 700));
const label = value.trim();
if (label === "error") {
throw new Error("Synthetic validation failure");
}
return label.length >= 3 && label !== "taken";
}
openButton.addEventListener("click", () => {
revision += 1;
dialog.returnValue = "";
status.textContent = "";
outcome.textContent = "";
dialog.showModal();
});
input.addEventListener("input", () => {
revision += 1;
});
cancelButton.addEventListener("click", () => dialog.close());
dialog.addEventListener("close", () => {
revision += 1;
outcome.textContent = "Dialog closed. Return value: " +
(dialog.returnValue || "(empty)");
});
form.addEventListener("submit", async event => {
if (allowNextSubmit) {
allowNextSubmit = false;
return;
}
event.preventDefault();
if (pending) return;
pending = true;
const submitter = event.submitter;
const value = input.value;
const checkedRevision = revision;
status.textContent = "Checking label...";
try {
const valid = await validateLabel(value);
if (!dialog.open) return;
if (revision !== checkedRevision || input.value !== value) {
status.textContent = "The label changed. Submit again.";
return;
}
if (!valid) {
status.textContent =
"Use at least three characters and avoid taken.";
return;
}
status.textContent = "Check passed. Submission requested.";
allowNextSubmit = true;
try {
if (submitter) form.requestSubmit(submitter);
else form.requestSubmit();
} finally {
allowNextSubmit = false;
}
} catch (error) {
if (dialog.open && revision === checkedRevision) {
status.textContent = "The check failed. Try again.";
}
console.error(error);
} finally {
pending = false;
}
});
</script>
</body>
</html>
O comportamento do navegador no exemplo decorre do ciclo de vida documentado. Uma pequena verificação com EventTarget do Node.js confirmou a diferença de tempo: o cancelamento síncrono alterou o resultado do despacho, enquanto o cancelamento após await não conseguiu alterá-lo. O exemplo completo com diálogo não foi executado aqui em um navegador de Mac.
Como a proteção preserva a ordem e lida com alterações?
No primeiro envio, o manipulador cancela imediatamente e captura tanto o rótulo quanto o botão usado para iniciar a ação. Enquanto a verificação está pendente, outro evento de envio também é cancelado, mas não inicia outra validação. Isso impede que uma sequência rápida de envios gere verificações simultâneas.
Quando a verificação é concluída, o manipulador confirma que o diálogo ainda está aberto e que o rótulo não foi alterado. O contador de revisões detecta edições do usuário mesmo que ele altere o texto e depois o restaure. A comparação dos valores também detecta uma alteração direta que deixe um texto diferente no campo.
Um resultado aprovado define allowNextSubmit imediatamente antes de chamar requestSubmit(). Esse método executa novamente o caminho de validação nativa e do evento submit. Quando o ouvinte é acionado novamente, ele consome a autorização e retorna sem cancelar. O navegador pode então executar a ação padrão de fechar o diálogo. Essa ordem segue a sequência do padrão: validação antes do evento submit. Padrão HTML: algoritmo de envio de formulários
O finally interno remove a autorização mesmo que nenhum evento submit ocorra ou que o método lance uma exceção. O finally externo limpa pending após uma rejeição, uma exceção, um resultado desatualizado ou uma nova tentativa bem-sucedida. Assim, uma tentativa malsucedida deixa o formulário disponível para outra verificação.
Fechar o diálogo também incrementa a revisão. Um resultado de uma abertura anterior não pode autorizar uma abertura posterior. Neste documento independente, essas verificações cobrem os caminhos de interação disponíveis. Se você adicionar mais campos ou scripts que os atualizem, amplie o snapshot e o tratamento de revisões para incluir cada valor do qual o validador depende.
Teste os caminhos de aprovação, rejeição e interrupção
Execute estas verificações no exemplo local do navegador. Os resultados esperados decorrem do código fornecido; não são apresentados como resultados de testes em navegadores.
| Ação | Resultado esperado |
|---|---|
Deixe o rótulo vazio e escolha Continuar |
A validação nativa do campo obrigatório bloqueia o envio antes da execução do manipulador assíncrono |
Digite ab e escolha Continuar |
O diálogo permanece aberto e exibe a mensagem de rejeição após a verificação |
Digite taken |
A regra simulada o rejeita |
Digite sample |
O diálogo permanece aberto durante a verificação e, depois, fecha com o valor de retorno accepted |
Digite error |
A mensagem de falha aparece e é possível enviar novamente |
| Envie um rótulo e edite-o durante a verificação | O resultado é descartado e o formulário solicita um novo envio |
| Envie repetidamente durante a verificação | Somente a primeira tentativa inicia uma verificação assíncrona |
Escolha Cancelar durante a verificação |
O diálogo fecha sem esperar pelo resultado pendente |
Tente também enviar pressionando Enter enquanto o campo de entrada está em foco. O envio implícito depende da estrutura do formulário e das convenções da plataforma; o motivo para tratar submit é cobrir a interação compatível que efetivamente acionar o envio do formulário. MDN: evento submit de HTMLFormElement
O que muda quando o formulário envia uma solicitação normal?
Em um formulário existente que envia solicitações, mantenha o action real, o method, os nomes dos campos, os valores dos botões e as substituições de envio. Substitua o validador simulado pela sua verificação assíncrona real. O código de abertura e fechamento do diálogo é específico desta demonstração; a sequência de cancelar, validar e solicitar o envio pertence ao manipulador de envio do formulário.
Mantenha os campos disponíveis para o envio final. Desativar os controles durante a espera exige cuidado: os campos desativados são excluídos quando o navegador constrói a lista de itens do envio. Em vez disso, o exemplo permite a edição e rejeita resultados de validação desatualizados. Padrão HTML: construção da lista de itens
Se houver outros ouvintes de submit, eles observarão tanto a primeira tentativa cancelada quanto a tentativa posterior. Coordene-os para que não façam o mesmo trabalho duas vezes nem alterem valores validados durante a passagem autorizada. requestSubmit() também não garante que o envio será concluído: outro ouvinte pode cancelá-lo, e as restrições nativas podem bloqueá-lo. Seu valor de retorno é undefined, não uma promessa de conclusão. MDN: HTMLFormElement.requestSubmit()
O contrato principal é preciso: cancelar de forma síncrona, validar um conjunto definido de valores, descartar resultados obsoletos e autorizar uma nova tentativa de envio nativo. Isso dá à validação assíncrona um lugar explícito no fluxo de trabalho, preservando o ciclo de vida do formulário no navegador.
Fontes
Padrão DOM — WHATWG
Padrão HTML: o elemento form — WHATWG
função async — MDN Web Docs
Evento: método preventDefault() — MDN Web Docs
HTMLFormElement: evento submit — MDN Web Docs
HTMLFormElement: método submit() — MDN Web Docs
HTMLFormElement: método requestSubmit() — MDN Web Docs
O elemento Dialog — MDN Web Docs
HTMLDialogElement: método showModal() — MDN Web Docs