Crear un formulario de contacto JavaScript con fetch
La mejora progresiva mantiene un respaldo POST nativo mientras JavaScript agrega un estado pendiente, errores en línea y una confirmación aceptada. El mismo identificador de intento hace que un reintento sin cambios sea seguro.
Crear un formulario gratisUn ejemplo copiable
<form action="https://littlereply.com/api/submit" method="POST" id="contact-js" data-locale="es" data-success="Hemos recibido tu mensaje.">
<input type="hidden" name="_fq_key" value="YOUR_ACCESS_KEY" />
<input type="text" name="botcheck" tabindex="-1" autocomplete="off" style="display:none" aria-hidden="true" />
<label for="contact-name">Nombre completo *</label>
<input type="text" name="name" id="contact-name" required />
<label for="contact-email">Correo electrónico *</label>
<input type="email" name="email" id="contact-email" required />
<label for="contact-message">Mensaje *</label>
<textarea name="message" id="contact-message" required rows="5"></textarea>
<button type="submit">Enviar mensaje</button>
<p role="status" aria-live="polite"></p>
</form>
<script>
const form = document.getElementById('contact-js');
const button = form.querySelector('button[type="submit"]');
const status = form.querySelector('[role="status"]');
let attempt = { content: '', id: '', accepted: false };
let busy = false;
function readBody() {
const body = new URLSearchParams();
for (const [name, value] of new FormData(form)) body.append(name, String(value));
return body;
}
form.addEventListener('input', () => {
if (!busy) {
button.disabled = attempt.accepted && attempt.content === readBody().toString();
if (!button.disabled) status.textContent = '';
}
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (busy || !form.reportValidity()) return;
const body = readBody();
const content = body.toString();
if (content !== attempt.content) attempt = { content, id: crypto.randomUUID(), accepted: false };
if (attempt.accepted) return;
busy = true;
button.disabled = true;
status.textContent = 'Enviando…';
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json', 'Idempotency-Key': attempt.id },
body
});
const result = await response.json();
if (!response.ok) throw new Error(response.status === 429
? 'Límite alcanzado. Espera antes de volver a intentarlo.'
: 'No se pudo enviar. Revisa los campos y vuelve a intentarlo.');
if (!result.data) throw new Error('No se pudo leer la confirmación. Comprueba la recepción antes de reintentar.');
attempt.accepted = true;
status.textContent = 'Tu mensaje se ha guardado. La entrega del correo se comprueba por separado.';
} catch (error) {
status.textContent = error instanceof Error ? error.message : 'No se pudo conectar. Vuelve a intentarlo.';
} finally {
busy = false;
button.disabled = attempt.accepted && attempt.content === readBody().toString();
}
});
</script>la clave YOUR_ACCESS_KEY es un marcador de posición para reemplazar con la clave pública de su formulario. El código no contiene clave de servidor ni dirección de destino.
Mantener el respaldo nativo
Déle al formulario una acción POST real y campos con nombre antes de agregar un detector de envío. Los visitantes todavía tienen una ruta de trabajo cuando el script no se carga.
Maneja la respuesta completa
Verifique Response.ok y analice el sobre de datos/error. Mostrar validación y limitar mensajes sin borrar campos. Anuncie cambios de estado a través de una región en vivo accesible.
Reintentar el mismo intento de forma segura
Cree la clave de idempotencia antes de la primera solicitud y reutilícela para reintentos sin cambios. Desbloquee el botón después de fallas conocidas y cree una nueva clave después de que el visitante edite el envío.
Prueba antes de publicar
Pruebe un mensaje válido, una dirección de correo electrónico no válida, un error de red y haga doble clic. Compruebe que solo aparezca un mensaje en su espacio de trabajo, que el texto sobreviva a un error y que el botón vuelva a ser utilizable. Pruebe desde un servidor local o un sitio web HTTPS, no solo un archivo abierto usando file://.
Este pequeño ejemplo cubre campos de texto. Para archivos o CAPTCHA, utilice el recorrido documentado y el código del panel: un archivo adjunto no es un simple campo JSON. No solucione CORS usando mode=no-cors, porque no podrá leer la confirmación.
Fuentes
Preguntas frecuentes
¿Por qué mantener la acción del formulario?
Proporciona un respaldo funcional y hace que el destino sea visible en el marcado.
¿Cuándo debo generar una nueva clave de reintento?
Cuando el contenido enviado cambia o después de una solicitud exitosa confirmada.
¿Puedo eliminar acción y método?
Guárdelos para que el envío de HTML siga funcionando cuando el script no esté disponible. La confirmación nativa difiere de la confirmación dentro de la página.