Llama a event.preventDefault() antes del primer await, ejecuta la validación asíncrona y luego usa form.requestSubmit() para iniciar un nuevo intento de envío. Una protección de un solo uso permite que ese segundo intento continúe sin repetir la comprobación asíncrona.
Este tutorial está pensado para alguien que edita una pequeña interfaz de navegador en un Mac. Crearás un ejemplo HTML local en el que un formulario cierra un cuadro de diálogo solo después de que se supera una comprobación sintética de la etiqueta. El mismo patrón de gestión de eventos se aplica a formularios normales que navegan o envían una solicitud.
¿Por qué un controlador asíncrono de envío no retrasa el envío?
El navegador no espera a que se resuelva la promesa devuelta por un detector de eventos. El algoritmo de envío de eventos del DOM invoca los detectores y continúa; el algoritmo de envío de HTML comprueba si se canceló el evento submit antes de continuar. Añadir async al detector no introduce una espera en ese ciclo de vida. Estándar DOM: envío de eventos, Estándar HTML: algoritmo de envío de formularios
Una función async se ejecuta sincrónicamente hasta su primer await y luego continúa de forma asíncrona. Por tanto, la cancelación debe realizarse antes del primer await, incluso cuando la promesa esperada ya se haya cumplido. Llamar a preventDefault() después no puede deshacer una decisión de envío que el navegador ya tomó. MDN: función async
La cancelación pone fin al intento original; no lo suspende para reanudarlo más adelante. Cuando la validación se supera, tu código debe solicitar explícitamente otro envío. Además, preventDefault() mantiene intacta la propagación del evento, por lo que otros detectores aún pueden observarlo. MDN: Event.preventDefault()
Escucha el evento submit del formulario. Esto abarca el envío mediante un botón, el envío implícito compatible, como al pulsar Enter, y las llamadas a requestSubmit(). Un controlador de clics que solo se aplica a botones no cubre todos los modos de envío. La validación nativa de restricciones suele ejecutarse primero: un campo obligatorio no válido impide el envío sin activar submit. MDN: evento submit de HTMLFormElement
¿Qué método conserva el comportamiento de envío del formulario?
Usa requestSubmit() cuando la comprobación superada deba volver al ciclo de vida normal del formulario. Si llamas directamente a submit(), se omiten tanto la validación de restricciones como el evento submit. Ese atajo cambia el contrato que debe preservar tu controlador asíncrono. MDN: HTMLFormElement.submit()
| Enfoque | Qué sucede | Idoneidad en este caso |
|---|---|---|
| Devolver una promesa desde el detector | El envío del evento continúa sin esperarla | No retrasa la acción predeterminada |
Cancelar y luego llamar a submit() |
El envío omite la validación nativa y los detectores de envío | Cambia el ciclo de vida |
Cancelar y luego llamar a requestSubmit(submitter) |
La validación nativa y el evento submit vuelven a ejecutarse | Conserva la ruta de envío prevista |
Captura event.submitter antes de esperar. Pasar ese botón a requestSubmit() conserva su función, incluidas las modificaciones del envío y su aporte de nombre/valor cuando corresponda. Si no había ningún botón de envío, llama a requestSubmit() sin argumentos. El botón indicado debe seguir perteneciendo al formulario; un botón no válido o que pertenezca a otro formulario provoca una excepción. MDN: HTMLFormElement.requestSubmit()
El nuevo envío vuelve a activar el detector. Por eso el ejemplo siguiente usa dos estados independientes: pending impide que se solapen las comprobaciones asíncronas, mientras que allowNextSubmit autoriza exactamente un paso sincrónico por el controlador. Una marca permanente de «validado» sería demasiado permisiva, porque modificaciones posteriores podrían reutilizar un resultado anterior.
Crea un ejemplo local completo en tu Mac
Guarda el siguiente documento completo como async-form.html en un editor de código de texto sin formato y luego ábrelo en un navegador actualizado en tu Mac. No hacen falta bibliotecas, importaciones, servidor ni instalación. El estándar indica que Safari admite requestSubmit() a partir de la versión 16. Estándar HTML: API de formularios
El ejemplo usa method="dialog" para que la acción predeterminada sea fácil de observar localmente. Si el envío se completa correctamente, se cierra el cuadro de diálogo contenedor y se establece su returnValue a partir del botón de envío; no se transmiten los controles del formulario. El botón de apertura llama a showModal() para mostrarlo. MDN: elemento dialog, MDN: HTMLDialogElement.showModal()
El validador es deliberadamente sintético: después de un breve temporizador, acepta etiquetas con al menos tres caracteres tras quitar los espacios al principio y al final, rechaza la etiqueta exacta en minúsculas taken y lanza una excepción para la etiqueta exacta en minúsculas error. La demora ilustra la finalización asíncrona; no es una medición del rendimiento.
<!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>
El comportamiento del navegador del ejemplo se deduce del ciclo de vida documentado. Una pequeña comprobación de EventTarget en Node.js confirmó la diferencia temporal: la cancelación sincrónica cambió el resultado del envío del evento, mientras que la cancelación posterior a await no llegó a tiempo para cambiarlo. Aquí no se ha ejecutado el ejemplo completo del cuadro de diálogo en un navegador de Mac.
¿Cómo conserva el orden la protección y cómo gestiona los cambios?
En el primer envío, el controlador cancela de inmediato y captura tanto la etiqueta como el botón que inició el envío. Mientras la comprobación está pendiente, también se cancela cualquier otro evento de envío, pero no se inicia otra validación. Así se evita que una serie rápida de envíos produzca comprobaciones solapadas.
Cuando se resuelve la comprobación, el controlador verifica que el cuadro de diálogo siga abierto y que la etiqueta no haya cambiado. El contador de revisiones detecta las ediciones del usuario incluso si cambia el texto y luego lo restaura. La comparación de valores también detecta un cambio directo en el valor que deje un texto distinto en el campo.
Un resultado válido establece allowNextSubmit justo antes de llamar a requestSubmit(). Ese método vuelve a ejecutar la validación nativa y la ruta del evento submit. Cuando se vuelve a invocar el detector, consume la autorización y retorna sin cancelar. Entonces el navegador puede ejecutar la acción predeterminada que cierra el cuadro de diálogo. Este orden sigue la secuencia del estándar, que realiza la validación antes del evento submit. Estándar HTML: algoritmo de envío de formularios
El finally interno elimina la autorización incluso si no se produce ningún evento submit o si el método lanza una excepción. El finally externo elimina pending tras un rechazo, una excepción, un resultado obsoleto o un reintento correcto. Por tanto, un intento fallido deja el formulario disponible para otra comprobación.
Cerrar el cuadro de diálogo también incrementa la revisión. Un resultado de una apertura anterior no puede autorizar una apertura posterior. En este documento independiente, estas comprobaciones cubren las interacciones disponibles. Si añades más campos o scripts que los actualicen, amplía la captura del estado y la gestión de revisiones para incluir todos los valores de los que dependa el validador.
Prueba las rutas de éxito, rechazo e interrupción
Ejecuta estas comprobaciones en el ejemplo local del navegador. Los resultados previstos se deducen del código proporcionado; no se presentan como resultados de pruebas en un navegador.
| Acción | Resultado previsto |
|---|---|
Deja la etiqueta vacía y elige Continue |
La validación nativa del campo obligatorio bloquea el envío antes de que se ejecute el controlador asíncrono |
Escribe ab y elige Continue |
El cuadro de diálogo permanece abierto y muestra el mensaje de rechazo tras la comprobación |
Escribe taken |
La regla sintética lo rechaza |
Escribe sample |
El cuadro de diálogo permanece abierto durante la comprobación y luego se cierra con el valor de retorno accepted |
Escribe error |
Aparece el mensaje de error y sigue siendo posible enviar otra vez |
| Envía una etiqueta y luego modifícala durante la comprobación | Se descarta el resultado y el formulario solicita otro envío |
| Envía varias veces mientras se comprueba | Solo el primer intento inicia una comprobación asíncrona |
Elige Cancel mientras se comprueba |
El cuadro de diálogo se cierra sin esperar el resultado pendiente |
Prueba también a enviar con Enter mientras el campo de entrada tiene el foco. El envío implícito depende de la estructura del formulario y de las convenciones de la plataforma; la razón para gestionar submit es cubrir cualquier interacción compatible que active el envío del formulario. MDN: evento submit de HTMLFormElement
¿Qué cambia cuando el formulario envía una solicitud normal?
En un formulario existente que envía solicitudes, conserva su action y method reales, los nombres de los campos, los valores de los botones y las modificaciones del envío. Sustituye el validador sintético por tu comprobación asíncrona real. El código para abrir y cerrar el cuadro de diálogo es específico de esta demostración; la secuencia de cancelar, validar y volver a solicitar el envío debe ir en el controlador de envío del formulario.
Mantén disponibles los campos para el envío final. Deshabilitar los controles durante la espera requiere cuidado: los campos deshabilitados quedan excluidos cuando el navegador construye la lista de valores del envío. En su lugar, el ejemplo permite seguir editando y rechaza los resultados de validación obsoletos. Estándar HTML: construcción de la lista de valores
Si existen otros detectores de envío, observarán tanto el primer intento cancelado como el posterior. Coordínalos para que no repitan el mismo trabajo ni cambien los valores validados durante el paso autorizado. requestSubmit() tampoco garantiza que el envío se complete: otro detector podría cancelarlo y las restricciones nativas podrían bloquearlo. Su valor de retorno es undefined, no una promesa de finalización. MDN: HTMLFormElement.requestSubmit()
El contrato fundamental es preciso: cancelar sincrónicamente, validar un conjunto definido de valores, descartar los resultados obsoletos y autorizar un nuevo intento de envío nativo. Así, la validación asíncrona tiene un lugar explícito en el flujo de trabajo y se conserva el ciclo de vida de los formularios del navegador.
Fuentes
Estándar DOM — WHATWG
Estándar HTML: el elemento form — WHATWG
función 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
El elemento Dialog — MDN Web Docs
HTMLDialogElement: método showModal() — MDN Web Docs