tutoriales.com

Svelte y Formularios Asíncronos: Gestión Eficiente de Peticiones y Respuestas 🚀

Este tutorial te guiará en la creación de formularios asíncronos en Svelte, abordando la gestión de estados de carga, errores y respuestas del servidor. Descubrirás cómo mejorar la experiencia de usuario y la reactividad de tus aplicaciones web.

Intermedio20 min de lectura13 views
Reportar error

Introducción: La Necesidad de Formularios Asíncronos en Svelte ✨

En el desarrollo web moderno, la interacción de los usuarios con formularios es fundamental. Ya sea para enviar datos de registro, comentarios, pedidos o búsquedas, los formularios son el puente entre el usuario y la lógica de negocio en el servidor. Tradicionalmente, enviar un formulario implicaba una recarga completa de la página, lo que resultaba en una experiencia de usuario (UX) deficiente y una percepción de lentitud.

Aquí es donde entran en juego los formularios asíncronos. Al utilizar JavaScript y técnicas como fetch o XMLHttpRequest, podemos enviar datos al servidor en segundo plano sin recargar la página. Esto permite mostrar estados de carga, manejar errores específicos y actualizar la interfaz de usuario de forma dinámica, proporcionando una UX mucho más fluida y profesional. Svelte, con su enfoque en la reactividad y la simplicidad, es una herramienta excelente para construir este tipo de formularios.

Este tutorial te equipará con los conocimientos y herramientas necesarias para crear formularios asíncronos robustos y reactivos en tus aplicaciones Svelte. Cubriremos desde la configuración básica hasta el manejo avanzado de estados y validación.

💡 Consejo: Un formulario asíncrono mejora drásticamente la percepción de velocidad y la fluidez de la interacción del usuario, evitando interrupciones innecesarias en el flujo de la aplicación.

Requisitos Previos 🛠️

Para sacar el máximo provecho de este tutorial, es recomendable tener conocimientos básicos de:

  • HTML y CSS: Fundamentales para la estructura y el estilo del formulario.
  • JavaScript: Nociones de ES6, funciones asíncronas (async/await) y manejo de promesas.
  • Svelte: Familiaridad con la sintaxis básica, reactividad, props y eventos.
  • Node.js y npm/pnpm/yarn: Para configurar un proyecto Svelte (si aún no lo tienes).

Configuración del Entorno de Desarrollo 🚀

Si ya tienes un proyecto Svelte o SvelteKit configurado, puedes saltar esta sección. De lo contrario, aquí te mostramos cómo empezar rápidamente.

1. Crear un Nuevo Proyecto SvelteKit

Recomendamos usar SvelteKit, el framework oficial de Svelte, ya que simplifica la creación de aplicaciones web robustas.

pnpm create svelte@latest my-async-form-app
cd my-async-form-app
pnpm install
pnpm dev

Durante la creación, puedes elegir las opciones que prefieras (TypeScript, ESLint, Prettier, etc.). Para este tutorial, una configuración básica de JavaScript es suficiente.

2. Estructura Básica del Proyecto

Una vez creado, tendrás una estructura de proyecto similar a esta:

my-async-form-app/
├── src/
│   ├── app.html
│   └── routes/
│       ├── +page.svelte
│       └── +layout.svelte
├── static/
├── svelte.config.js
├── vite.config.js
├── package.json
└── ...

Trabajaremos principalmente en el archivo src/routes/+page.svelte para nuestro formulario de ejemplo.

Fundamentos de la Comunicación Asíncrona con fetch 🌐

Antes de sumergirnos en Svelte, es crucial entender cómo funciona la comunicación asíncrona en JavaScript, específicamente usando la API fetch.

fetch es una interfaz moderna para realizar solicitudes de red. Devuelve una Promise que resuelve con el objeto Response.

Sintaxis Básica de fetch

fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ key: 'value' }),
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Usaremos async/await para una sintaxis más limpia y fácil de leer.

async function postData(url = '', data = {}) {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Fetch error:', error);
    throw error; // Re-lanzar para manejar en el componente
  }
}

// Uso:
// postData('/api/submit', { name: 'Juan' })
//   .then(data => console.log(data))
//   .catch(error => console.error(error));
📌 Nota: Es vital verificar `response.ok` para asegurar que la solicitud HTTP fue exitosa (código de estado 2xx). La promesa de `fetch` solo se rechazará si hay un error de red o algo impide que la solicitud sea completada. Los errores de servidor (ej. 404, 500) aún resuelven la promesa, pero `response.ok` será `false`.

Creando el Formulario Básico en Svelte 📝

Vamos a empezar con un formulario simple en src/routes/+page.svelte.

<!-- src/routes/+page.svelte -->
<script>
  let name = '';
  let email = '';

  function handleSubmit() {
    console.log('Datos del formulario:', { name, email });
    // Aquí es donde haremos la llamada asíncrona
  }
</script>

<h1>Formulario de Contacto Asíncrono</h1>

<form on:submit|preventDefault={handleSubmit}>
  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" bind:value={name} required />
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" bind:value={email} required />
  </div>

  <button type="submit">Enviar</button>
</form>

<style>
  form {
    display: flex;
    flex-direction: column;
    gap: 15px;
    max-width: 400px;
    margin: 20px auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }

  div {
    display: flex;
    flex-direction: column;
  }

  label {
    margin-bottom: 5px;
    font-weight: bold;
  }

  input[type="text"], input[type="email"] {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 1rem;
  }

  button {
    padding: 10px 15px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1rem;
    transition: background-color 0.2s ease;
  }

  button:hover {
    background-color: #0056b3;
  }
</style>
🔥 Importante: Usamos `on:submit|preventDefault={handleSubmit}`. El modificador `preventDefault` es crucial para evitar que el navegador realice su acción por defecto de recargar la página al enviar el formulario.

Implementando la Lógica Asíncrona y Gestión de Estados 🔄

Ahora, integraremos la llamada fetch y gestionaremos los estados del formulario: loading, error y success.

1. Variables de Estado

Necesitaremos variables para representar el estado de nuestra solicitud:

  • isLoading: un booleano para indicar si la solicitud está en curso.
  • error: una cadena para almacenar mensajes de error.
  • successMessage: una cadena para mostrar mensajes de éxito.
<script>
  let name = '';
  let email = '';

  let isLoading = false;
  let error = '';
  let successMessage = '';

  // ... (función handleSubmit)
</script>

2. Modificando handleSubmit

Actualizaremos la función handleSubmit para realizar la petición asíncrona.

<!-- ... dentro de la etiqueta <script> ... -->

async function handleSubmit() {
    isLoading = true;
    error = ''; // Limpiar errores previos
    successMessage = ''; // Limpiar mensajes de éxito previos

    try {
        const response = await fetch('/api/submit', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ name, email }),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || `Error del servidor: ${response.status}`);
        }

        const result = await response.json();
        successMessage = result.message || 'Formulario enviado con éxito!';

        // Opcional: limpiar el formulario después de un envío exitoso
        name = '';
        email = '';

    } catch (err) {
        error = err.message || 'Ocurrió un error al enviar el formulario.';
    } finally {
        isLoading = false;
    }
}

<!-- ... -->

3. Mostrando los Estados en la UI

Ahora, actualizaremos el HTML para mostrar los indicadores de carga, mensajes de error y mensajes de éxito.

<!-- ... después del formulario ... -->

{#if isLoading}
  <p class="loading-message">Enviando formulario...</p>
{/if}

{#if error}
  <p class="error-message">⚠️ {error}</p>
{/if}

{#if successMessage}
  <p class="success-message">✅ {successMessage}</p>
{/if}

<!-- ... en la sección <style> ... -->
<style>
  /* ... estilos existentes ... */

  .loading-message {
    color: #007bff;
    text-align: center;
    margin-top: 10px;
  }

  .error-message {
    color: #dc3545;
    text-align: center;
    margin-top: 10px;
    font-weight: bold;
  }

  .success-message {
    color: #28a745;
    text-align: center;
    margin-top: 10px;
    font-weight: bold;
  }

  button:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
  }
</style>

Además, deshabilitaremos el botón de envío mientras la solicitud está en curso para evitar envíos duplicados:

<button type="submit" disabled={isLoading}>{isLoading ? 'Enviando...' : 'Enviar'}</button>
🔥 Importante: El estado `isLoading` se usa no solo para el mensaje, sino también para deshabilitar el botón, mejorando la UX al prevenir clics múltiples y solicitudes redundantes.

Simulación de un Backend API (SvelteKit) 🧪

Para probar nuestro formulario sin necesidad de un backend real, podemos simular una API usando los endpoints de SvelteKit.

Crea un archivo src/routes/api/submit/+server.js (o .ts si usas TypeScript) con el siguiente contenido:

// src/routes/api/submit/+server.js

export async function POST({ request }) {
    const data = await request.json();

    console.log('Datos recibidos en el servidor simulado:', data);

    // Simular un retardo de red
    await new Promise(resolve => setTimeout(resolve, 1500));

    // Simular un error el 20% de las veces para propósitos de prueba
    if (Math.random() < 0.2) {
        return new Response(JSON.stringify({ message: 'Error de validación simulado.' }), {
            status: 400,
            headers: {
                'Content-Type': 'application/json'
            }
        });
    }

    // Simular éxito
    return new Response(JSON.stringify({ message: 'Datos recibidos con éxito!', receivedData: data }), {
        status: 200,
        headers: {
            'Content-Type': 'application/json'
        }
    });
}

Ahora, cuando envíes el formulario, la solicitud irá a este endpoint simulado. Podrás ver los mensajes de carga, éxito y, ocasionalmente, el mensaje de error.

Usuario llena formulario Click en Enviar (Deshabilita botón y muestra loading) Fetch (POST /api/submit) ¿Estado HTTP? Backend responde 200 OK Muestra éxito y limpia formulario Backend responde 4xx/5xx Muestra mensaje de error ÉXITO ERROR

Validación del Formulario en el Cliente y Servidor ✅

Una buena aplicación requiere validación tanto en el cliente (para una UX inmediata) como en el servidor (por seguridad y fiabilidad).

1. Validación en el Cliente (Svelte)

Podemos añadir lógica de validación básica en el cliente antes de enviar la solicitud.

<script>
  // ... variables de estado existentes ...

  let name = '';
  let email = '';
  
  let nameError = '';
  let emailError = '';

  function validateForm() {
    nameError = '';
    emailError = '';
    let isValid = true;

    if (!name.trim()) {
      nameError = 'El nombre es obligatorio.';
      isValid = false;
    }

    if (!email.trim()) {
      emailError = 'El email es obligatorio.';
      isValid = false;
    } else if (!/^[^	
 ]+@[^	
 ]+\.[^	
 ]+$/.test(email)) { // Expresión regular simple para email
      emailError = 'El email no es válido.';
      isValid = false;
    }

    return isValid;
  }

  async function handleSubmit() {
    // Primero, validar en el cliente
    if (!validateForm()) {
      return; // Detener el envío si la validación falla
    }

    // ... (resto de la lógica de envío asíncrono)
  }
</script>

<!-- ... en el HTML del formulario ... -->

  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" bind:value={name} required />
    {#if nameError}
      <p class="validation-error">{nameError}</p>
    {/if}
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" bind:value={email} required />
    {#if emailError}
      <p class="validation-error">{emailError}</p>
    {/if}
  </div>

<!-- ... en la sección <style> ... -->
<style>
  /* ... estilos existentes ... */

  .validation-error {
    color: #dc3545;
    font-size: 0.85em;
    margin-top: 5px;
  }

  input.invalid {
    border-color: #dc3545;
  }
</style>

Para aplicar el estilo invalid a los inputs cuando hay errores:

<input type="text" id="name" bind:value={name} required class:invalid={!!nameError} />
<input type="email" id="email" bind:value={email} required class:invalid={!!emailError} />
💡 Consejo: Para validaciones más complejas, considera usar librerías de validación como [Zod](https://zod.dev/) o [Yup](https://github.com/jquense/yup) combinadas con Svelte, o soluciones específicas para formularios como [Superforms](https://superforms.rocks/) en SvelteKit.

2. Validación en el Servidor (SvelteKit Endpoint)

Nuestro endpoint de ejemplo ya tiene una simulación de error. En un backend real, harías validaciones más exhaustivas. Si la validación falla, el servidor debería responder con un código de estado apropiado (ej. 400 Bad Request) y un mensaje de error detallado.

// src/routes/api/submit/+server.js (mejorado para validación)

export async function POST({ request }) {
    const data = await request.json();

    console.log('Datos recibidos en el servidor simulado:', data);

    // Simular un retardo de red
    await new Promise(resolve => setTimeout(resolve, 1500));

    // Validación básica en el 'servidor'
    if (!data.name || data.name.trim().length < 3) {
        return new Response(JSON.stringify({ message: 'El nombre debe tener al menos 3 caracteres.' }), {
            status: 400,
            headers: { 'Content-Type': 'application/json' }
        });
    }

    if (!data.email || !/^[^	
 ]+@[^	
 ]+\.[^	
 ]+$/.test(data.email)) {
        return new Response(JSON.stringify({ message: 'El formato del email no es válido.' }), {
            status: 400,
            headers: { 'Content-Type': 'application/json' }
        });
    }

    // Simular éxito
    return new Response(JSON.stringify({ message: 'Datos recibidos con éxito!', receivedData: data }), {
        status: 200,
        headers: { 'Content-Type': 'application/json' }
    });
}

Cuando el servidor devuelve un error 400, nuestra función handleSubmit lo captura en el catch y el mensaje de error del servidor se mostrará al usuario.


Mejorando la Experiencia de Usuario 📈

Vamos a añadir algunas mejoras para una UX aún mejor.

1. Limpieza Automática de Mensajes

Los mensajes de éxito o error pueden desaparecer después de un tiempo para no saturar la interfaz.

<script>
  // ... variables de estado existentes ...

  let timeoutId = null; // Para almacenar el ID del temporizador

  async function handleSubmit() {
    // Limpiar temporizador existente si lo hay
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }

    // ... (resto de la lógica de envío asíncrono)

    try {
      // ... lógica de fetch y manejo de respuesta ...

      if (successMessage) {
        timeoutId = setTimeout(() => {
          successMessage = '';
        }, 5000); // El mensaje de éxito desaparece después de 5 segundos
      }
    } catch (err) {
      // ... manejo de errores ...
      if (error) {
        timeoutId = setTimeout(() => {
          error = '';
        }, 7000); // El mensaje de error desaparece después de 7 segundos
      }
    } finally {
      isLoading = false;
    }
  }
</script>

2. Resetear el Formulario y Mensajes al Recibir Éxito

Ya estamos limpiando el formulario (name = ''; email = '';). Esto es bueno. Asegurarse de que los mensajes de error también se limpian es crucial para la reactividad.

<script>
  // ... (dentro de handleSubmit, en el bloque try, después de successMessage = ...) ...

  // Limpiar errores de validación del cliente también
  nameError = '';
  emailError = '';
</script>

3. Feedback Visual Adicional

Podemos añadir un pequeño icono de carga al botón para mayor claridad.

<button type="submit" disabled={isLoading}>
  {#if isLoading}
    <div class="spinner"></div> Enviando...
  {:else}
    Enviar
  {/if}
</button>

<style>
  /* ... estilos existentes ... */

  .spinner {
    border: 3px solid #f3f3f3;
    border-top: 3px solid #0056b3;
    border-radius: 50%;
    width: 15px;
    height: 15px;
    animation: spin 1s linear infinite;
    display: inline-block;
    vertical-align: middle;
    margin-right: 8px;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
</style>
Más sobre Feedback Visual Además de un spinner, considera:
  • Cambiar el color del botón o su opacidad mientras está deshabilitado.
  • Usar una animación sutil para los mensajes de éxito/error cuando aparecen.
  • Implementar un `` bar si la operación es muy larga y se puede medir su progreso.

Refactorización: Componente AsyncForm Reutilizable 📦

Para mantener nuestro código limpio y reutilizable, podemos encapsular la lógica del formulario asíncrono en un componente Svelte.

1. Crear src/lib/AsyncForm.svelte

<!-- src/lib/AsyncForm.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';

  export let onSubmit = async () => {}; // Función que el componente padre pasará para manejar el envío
  export let buttonText = 'Enviar';
  export let initialData = {}; // Para inicializar campos si se desea
  export let validationSchema = null; // Un esquema de validación opcional

  const dispatch = createEventDispatcher();

  let isLoading = false;
  let error = '';
  let successMessage = '';
  let clientErrors = {}; // Para errores de validación del cliente

  // Exponemos las variables de estado para que el padre pueda reaccionar a ellas
  $: dispatch('loading', isLoading);
  $: dispatch('error', error);
  $: dispatch('success', successMessage);

  // Función de validación interna, si se proporciona un esquema
  function validateClient(formData) {
    if (!validationSchema) return true; // No hay esquema, no hay validación
    clientErrors = {};
    let isValid = true;

    for (const key in validationSchema) {
      const rules = validationSchema[key];
      const value = formData[key];

      if (rules.required && !value.trim()) {
        clientErrors[key] = rules.requiredMessage || `${key} es obligatorio.`;
        isValid = false;
      } else if (rules.pattern && !rules.pattern.test(value)) {
        clientErrors[key] = rules.patternMessage || `${key} no es válido.`;
        isValid = false;
      }
      // Añadir más reglas de validación según sea necesario
    }
    return isValid;
  }

  async function handleInternalSubmit(event) {
    event.preventDefault();

    isLoading = true;
    error = '';
    successMessage = '';
    clientErrors = {};

    const formData = new FormData(event.target);
    const data = Object.fromEntries(formData.entries());

    // Validar en el cliente antes de enviar
    if (!validateClient(data)) {
      isLoading = false;
      return; // Detener el envío si falla la validación del cliente
    }

    try {
      const result = await onSubmit(data); // Llamar a la función onSubmit del padre
      successMessage = result?.message || 'Operación completada con éxito!';
      dispatch('submitSuccess', result); // Notificar al padre del éxito

      // Limpiar mensajes después de un tiempo
      setTimeout(() => successMessage = '', 5000);

    } catch (err) {
      error = err.message || 'Ocurrió un error.';
      dispatch('submitError', err); // Notificar al padre del error

      // Limpiar mensajes después de un tiempo
      setTimeout(() => error = '', 7000);
    } finally {
      isLoading = false;
    }
  }

  // Se expone `clientErrors` y `isLoading` a través de un store o context si son necesarios para los hijos.
  // Para este ejemplo, solo los pasamos a través de dispatch para reactividad externa.

</script>

<form on:submit={handleInternalSubmit}>
  <!-- El contenido del formulario (slots) irá aquí -->
  <slot 
    isLoading={isLoading} 
    error={error} 
    successMessage={successMessage} 
    clientErrors={clientErrors}
  />

  <button type="submit" disabled={isLoading}>
    {#if isLoading}
      <div class="spinner"></div> {buttonText === 'Enviar' ? 'Enviando...' : `Cargando ${buttonText.toLowerCase()}...`}
    {:else}
      {buttonText}
    {/if}
  </button>

  {#if error}
    <p class="error-message">⚠️ {error}</p>
  {/if}

  {#if successMessage}
    <p class="success-message">✅ {successMessage}</p>
  {/if}
</form>

<style>
  form {
    display: flex;
    flex-direction: column;
    gap: 15px;
    max-width: 400px;
    margin: 20px auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }

  button {
    padding: 10px 15px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1rem;
    transition: background-color 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  button:hover:not(:disabled) {
    background-color: #0056b3;
  }

  button:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
  }

  .spinner {
    border: 3px solid #f3f3f3;
    border-top: 3px solid #0056b3;
    border-radius: 50%;
    width: 15px;
    height: 15px;
    animation: spin 1s linear infinite;
    display: inline-block;
    vertical-align: middle;
    margin-right: 8px;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }

  .error-message {
    color: #dc3545;
    text-align: center;
    margin-top: 10px;
    font-weight: bold;
  }

  .success-message {
    color: #28a745;
    text-align: center;
    margin-top: 10px;
    font-weight: bold;
  }
</style>

2. Usando AsyncForm en src/routes/+page.svelte

<!-- src/routes/+page.svelte -->
<script>
  import AsyncForm from '$lib/AsyncForm.svelte';

  let name = '';
  let email = '';

  // Esquema de validación para el componente AsyncForm
  const validationSchema = {
    name: { required: true, requiredMessage: 'El nombre es requerido.' },
    email: { required: true, requiredMessage: 'El email es requerido.', pattern: /^[^	
 ]+@[^	
 ]+\.[^	
 ]+$/, patternMessage: 'El email no es válido.' }
  };

  async function submitContactForm(formData) {
    console.log('Enviando desde el componente padre:', formData);
    // Esta es la función que se llamará cuando el formulario interno se envíe
    // Aquí haríamos la llamada fetch real a nuestro backend
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(formData),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || `Error del servidor: ${response.status}`);
    }

    const result = await response.json();
    name = ''; // Limpiar campos después de éxito
    email = '';
    return result; // Devuelve el resultado para que AsyncForm lo muestre o lo maneje
  }

  // Podemos reaccionar a los eventos emitidos por AsyncForm
  function handleSuccess(event) {
    console.log('Formulario enviado exitosamente:', event.detail);
  }

  function handleError(event) {
    console.error('Error al enviar formulario:', event.detail);
  }

  // Variables para mostrar los estados del formulario globalmente si es necesario
  let currentIsLoading = false;
  let currentError = '';
  let currentSuccessMessage = '';
</script>

<h1>Formulario de Contacto Asíncrono Reutilizable</h1>

<AsyncForm 
  onSubmit={submitContactForm} 
  buttonText="Guardar Datos"
  {validationSchema}
  on:submitSuccess={handleSuccess}
  on:submitError={handleError}
  on:loading={(e) => currentIsLoading = e.detail}
  on:error={(e) => currentError = e.detail}
  on:success={(e) => currentSuccessMessage = e.detail}
>
  <!-- Los inputs se colocan dentro del slot -->
  <div let:clientErrors={errors}>
    <label for="name">Nombre:</label>
    <input type="text" id="name" name="name" bind:value={name} class:invalid={!!errors.name} />
    {#if errors.name}
      <p class="validation-error">{errors.name}</p>
    {/if}
  </div>

  <div let:clientErrors={errors}>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" bind:value={email} class:invalid={!!errors.email} />
    {#if errors.email}
      <p class="validation-error">{errors.email}</p>
    {/if}
  </div>
</AsyncForm>

{#if currentIsLoading}
  <p>El formulario externo sabe que está cargando...</p>
{/if}
{#if currentError}
  <p>El formulario externo ha detectado un error: {currentError}</p>
{/if}
{#if currentSuccessMessage}
  <p>El formulario externo ha detectado un éxito: {currentSuccessMessage}</p>
{/if}

<style>
  /* Estilos para los mensajes de error de validación */
  .validation-error {
    color: #dc3545;
    font-size: 0.85em;
    margin-top: 5px;
  }

  input.invalid {
    border-color: #dc3545;
  }
  /* Los estilos generales del formulario y botón están en AsyncForm.svelte */
</style>
📌 Nota: Usamos `let:clientErrors={errors}` en el slot para exponer las validaciones del cliente del componente `AsyncForm` a los inputs internos. Esto es una característica potente de los slots con props.
Pagina Principal (+page.svelte) Props: onSubmit, validationSchema Eventos: submitSuccess, submitError AsyncForm.svelte Estado interno: isLoading error success <Slot /> Inputs del formulario Email Password

Consideraciones Avanzadas y Mejores Prácticas 🎯

1. Manejo de AbortController

Si el usuario envía el formulario rápidamente varias veces, o navega antes de que se complete la solicitud, podemos cancelar las peticiones pendientes para evitar condiciones de carrera o fugas de memoria. AbortController es la herramienta para esto.

<script>
  let abortController;

  async function handleSubmit() {
    if (abortController) {
      abortController.abort(); // Cancela cualquier petición anterior pendiente
    }
    abortController = new AbortController();
    const signal = abortController.signal;

    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name, email }),
        signal, // Pasar la señal al fetch
      });

      // ... (resto del código)

    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('Fetch request aborted');
        return; // No mostrar error al usuario si fue abortado intencionadamente
      }
      // ... (manejo de otros errores)
    } finally {
      isLoading = false;
      abortController = null; // Limpiar después de que la petición se complete o aborte
    }
  }
</script>
⚠️ Advertencia: El `AbortController` es crucial para evitar efectos secundarios indeseados cuando los usuarios interactúan rápidamente con el formulario o abandonan la página antes de que la petición termine.

2. Mejora Progresiva

Para garantizar que el formulario funcione incluso sin JavaScript (o si falla), podemos usar la mejora progresiva. En SvelteKit, esto se maneja de forma elegante a través de las acciones de formulario (form actions).

Aunque este tutorial se centra en formularios asíncronos controlados por JS, es bueno saber que SvelteKit permite una ruta con mejora progresiva. Una acción de formulario se envía por defecto con una recarga de página, pero SvelteKit la intercepta y la convierte en una petición asíncrona si JS está habilitado.

Para usar acciones de formulario, necesitarías modificar el endpoint +server.js a +page.server.js y usar export const actions.

// src/routes/api/submit/+page.server.js (ejemplo con acciones de formulario)
import { fail } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const name = data.get('name');
    const email = data.get('email');

    // Validaciones del servidor
    if (!name || name.length < 3) {
      return fail(400, { name, email, error: 'El nombre es obligatorio y debe tener al menos 3 caracteres.' });
    }
    if (!email || !/^[^	
 ]+@[^	
 ]+\.[^	
 ]+$/.test(email)) {
      return fail(400, { name, email, error: 'El formato del email no es válido.' });
    }

    // Simular retardo
    await new Promise(resolve => setTimeout(resolve, 1500));

    console.log('Datos de formulario action:', { name, email });
    return { success: true, message: 'Datos recibidos con éxito!' };
  }
};

Y en tu Svelte component:

<script>
  // No necesitas un handleSubmit manual; SvelteKit lo hace por ti
</script>

<!-- Esto es un formulario estándar, pero SvelteKit lo mejora automáticamente -->
<form method="POST" action="/api/submit">
  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" name="name" required />
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required />
  </div>

  <button type="submit">Enviar</button>
</form>

<!-- SvelteKit proporciona datos de formulario en $page.form para mostrar errores/éxito -->
{#if $page.form?.error}
  <p class="error-message">{$page.form.error}</p>
{/if}
{#if $page.form?.success}
  <p class="success-message">{$page.form.message}</p>
{/if}
🔥 Importante: Para formularios más complejos y una integración más profunda con SvelteKit, explora las `form actions` y librerías como Superforms. Son el camino recomendado para una DX y UX óptimas en SvelteKit.

3. Gestión de Tokens y Autenticación

En aplicaciones reales, las solicitudes a la API a menudo requieren tokens de autenticación (JWT, OAuth). Estos se suelen añadir en los headers de la petición fetch.

const token = 'tu_token_de_autenticacion_aqui'; // Obtener de localStorage, store, etc.

const response = await fetch('/api/secure-submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}` // Añadir el token de autenticación
    },
    body: JSON.stringify(data),
    signal,
});

Resumen y Próximos Pasos 📖

Hemos cubierto un camino completo para construir y gestionar formularios asíncronos en Svelte:

  1. Fundamentos de fetch: Entendiendo cómo hacer peticiones HTTP asíncronas.
  2. Formulario Básico en Svelte: Enlazar inputs y prevenir el comportamiento por defecto.
  3. Gestión de Estados: Implementar isLoading, error y successMessage para feedback de usuario.
  4. Simulación de Backend: Usar SvelteKit endpoints para probar sin un servidor real.
  5. Validación: Añadir validación del lado del cliente y del servidor.
  6. Mejoras de UX: Limpieza de mensajes, spinners de carga y deshabilitar botones.
  7. Componente Reutilizable: Encapsular la lógica en un componente AsyncForm.svelte.
  8. Consideraciones Avanzadas: AbortController y form actions para robustez y mejora progresiva.

Los formularios asíncronos son un pilar en las Single Page Applications (SPA) y los Progressive Web Apps (PWA). Dominar su creación y gestión te permitirá construir interfaces de usuario rápidas, responsivas y fiables.

¿Qué sigue? 🤔

  • Internacionalización (i18n): Traduce tus mensajes de error y éxito.
  • Librerías de Formulario: Explora soluciones más completas como Superforms para SvelteKit, que integra acciones de formulario, validación de esquema (Zod, Yup), y gestión de estado de forma reactiva.
  • Tests: Escribe pruebas unitarias y de integración para tus componentes de formulario y la lógica de envío.
  • Accesibilidad: Asegúrate de que tus formularios sean accesibles para todos los usuarios, incluyendo el uso de atributos aria-live para mensajes de estado.

¡Sigue construyendo y mejorando tus habilidades con Svelte! 🚀

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!