tutoriales.com

SvelteKit y Formularios Dinámicos: Construyendo Interacciones Modernas con Mejora Progresiva 🚀

Este tutorial profundiza en la creación y gestión de formularios dinámicos en SvelteKit, cubriendo desde la mejora progresiva hasta la validación avanzada y el manejo de envíos. Aprenderás a construir formularios accesibles, reactivos y robustos, aprovechando al máximo las capacidades de SvelteKit para una experiencia de usuario excepcional.

Intermedio20 min de lectura10 views
Reportar error

Introducción: El Poder de los Formularios Dinámicos en SvelteKit ✨

Los formularios son el corazón de casi cualquier aplicación web interactiva. Desde registros de usuario hasta interfaces de administración complejas, la forma en que manejamos los datos de entrada es crucial para una experiencia de usuario fluida y segura. En el ecosistema de SvelteKit, tenemos herramientas poderosas para construir formularios no solo funcionales sino también dinámicos, reactivos y con una sólida base de mejora progresiva.

Este tutorial te guiará a través de las mejores prácticas para desarrollar formularios en SvelteKit, asegurando que tu aplicación sea robusta, accesible y ofrezca una excelente experiencia de usuario, incluso sin JavaScript inicial.

🔥 Importante: La mejora progresiva es clave para la resiliencia de tu aplicación. Un formulario bien construido debería funcionar incluso con JavaScript deshabilitado, proporcionando una capa extra de funcionalidad cuando JavaScript está disponible.

1. Fundamentos de Formularios en SvelteKit: La Mejora Progresiva como Pilar 🏗️

Antes de sumergirnos en la reactividad, es fundamental entender cómo SvelteKit maneja los envíos de formularios por defecto. SvelteKit aprovecha la arquitectura estándar de HTML, donde un formulario (<form>) realiza una solicitud HTTP cuando se envía, típicamente un POST. Esto es la base de la mejora progresiva.

1.1. El Enfoque Clásico de HTML

Consideremos un formulario HTML básico:

<form method="POST" action="?/submit">
  <label for="name">Nombre:</label>
  <input type="text" id="name" name="name" required>
  <label for="email">Email:</label>
  <input type="email" id="email" name="email" required>
  <button type="submit">Enviar</button>
</form>

Cuando este formulario se envía, el navegador envía una solicitud POST a la URL /current-page?submit. SvelteKit intercepta esta solicitud en el +page.server.js o +server.js correspondiente.

1.2. Manejando Envíos con +page.server.js 🚦

Dentro de +page.server.js, puedes definir acciones para manejar diferentes envíos de formularios. SvelteKit utiliza el concepto de acciones (actions) para esto.

// src/routes/my-form/+page.server.js
import { fail, redirect } from '@sveltejs/kit';

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

    // Lógica de validación básica
    if (!name || !email) {
      return fail(400, { success: false, message: 'Ambos campos son requeridos.' });
    }

    // Simular guardado en base de datos
    console.log('Datos recibidos:', { name, email });

    // Redirigir al usuario o devolver un mensaje de éxito
    return { success: true, message: 'Formulario enviado con éxito!' };
  },

  // Puedes tener múltiples acciones con nombres personalizados
  // por ejemplo, action="?/saveSettings"
  saveSettings: async ({ request }) => {
    // ... otra lógica ...
    return { success: true, message: 'Configuración guardada.' };
  }
};

El default action se ejecuta cuando no se especifica un action="?/actionName". Si la acción se llama, por ejemplo, saveSettings, tu formulario debería apuntar a action="?/saveSettings".

1.3. Accediendo a los Datos de la Acción en +page.svelte 📤

Después de que una acción se ejecuta, los datos que devuelve están disponibles en el +page.svelte a través de la propiedad form en el objeto data (que es el valor de la propiedad export let data en el script del componente).

<!-- src/routes/my-form/+page.svelte -->
<script lang="ts">
  export let data;
  import type { ActionData } from './$types';

  // type ActionData = typeof import('./$types').PageServerData['form']; // Esta sería la forma correcta en TS 5.0+
  // Sin embargo, SvelteKit ya provee el tipo ActionData para la acción por defecto.

  let form: ActionData = data.form; // Accedemos a los datos devueltos por la acción
</script>

<h1>Mi Formulario Dinámico</h1>

{#if form?.success}
  <div class="callout tip">✅ <strong>Éxito:</strong> {form.message}</div>
{:else if form?.message}
  <div class="callout warning">⚠️ <strong>Error:</strong> {form.message}</div>
{/if}

<form method="POST" action="?/" novalidate>
  <label for="name">Nombre:</label>
  <input type="text" id="name" name="name" required value={form?.name || ''}>
  <label for="email">Email:</label>
  <input type="email" id="email" name="email" required value={form?.email || ''}>
  <button type="submit">Enviar</button>
</form>

<details open>
  <summary>Contenido de 'data'</summary>
  <pre><code>{JSON.stringify(data, null, 2)}</code></pre>
</details>

Observa cómo el value de los inputs puede ser pre-llenado con form?.name || ''. Esto es crucial para rellenar el formulario con los datos enviados si hubo un error de validación, evitando que el usuario tenga que volver a escribir todo.


2. Añadiendo Interactividad con use:enhance y Svelte 💡

Ahora que tenemos la base de la mejora progresiva, es hora de añadir interactividad sin sacrificarla. SvelteKit proporciona la directiva use:enhance para interceptar los envíos de formularios y manejarlos con JavaScript, sin una recarga de página completa.

2.1. Implementando use:enhance

Para usar use:enhance, primero necesitas importarlo de @sveltejs/kit/forms.

<!-- src/routes/my-form/+page.svelte (modificado) -->
<script lang="ts">
  import { enhance } from '@sveltejs/kit/forms';
  export let data;
  import type { ActionData } from './$types';

  let form: ActionData = data.form;
  let isLoading = false; // Estado para el indicador de carga

  // Función para manejar el inicio y fin del envío
  function handleEnhance() {
    isLoading = true;
    return async ({ result, update }) => {
      // Aquí puedes manejar la respuesta de la acción
      if (result.type === 'success') {
        console.log('Formulario enviado con éxito AJAX!');
        // Podrías limpiar el formulario aquí, por ejemplo
      } else if (result.type === 'error') {
        console.error('Error al enviar formulario AJAX:', result.error);
      }
      await update(); // Actualiza los datos de la página y el componente
      isLoading = false;
    };
  }
</script>

<h1>Mi Formulario Dinámico con `enhance`</h1>

{#if form?.success}
  <div class="callout tip">✅ <strong>Éxito:</strong> {form.message}</div>
{:else if form?.message}
  <div class="callout warning">⚠️ <strong>Error:</strong> {form.message}</div>
{/if}

<form method="POST" action="?/" use:enhance={handleEnhance} novalidate>
  <label for="name">Nombre:</label>
  <input type="text" id="name" name="name" required value={form?.name || ''}>
  <label for="email">Email:</label>
  <input type="email" id="email" name="email" required value={form?.email || ''}>
  <button type="submit" disabled={isLoading}>
    {#if isLoading}Enviando...{:else}Enviar{/if}
  </button>
</form>

Con use:enhance, el formulario se envía vía Fetch API (AJAX) en lugar de una recarga completa de la página. El callback handleEnhance te permite controlar el estado de la UI (ej. isLoading) y reaccionar a la respuesta del servidor (result).

📌 Nota: La directiva `novalidate` en el formulario deshabilita la validación nativa del navegador, lo que nos da control total sobre la validación en el cliente y en el servidor.

2.2. Feedback Visual y Estado de Carga 🔄

El ejemplo anterior ya incluye un indicador de carga (isLoading). Puedes expandirlo para mostrar un spinner o deshabilitar el formulario mientras se envía.

Inicio Usuario envía formulario ¿Enhance intercepta? No Envío HTTP normal Muestra 'cargando' Envío AJAX Recibe respuesta Oculta 'cargando' Actualiza UI

3. Validación de Formularios: Cliente y Servidor 🛡️

Una buena estrategia de validación involucra tanto el lado del cliente como el del servidor. La validación del cliente proporciona feedback instantáneo, mientras que la validación del servidor es la línea de defensa final contra datos maliciosos o incorrectos.

3.1. Validación del Lado del Cliente (JavaScript) ✅

Podemos añadir lógica de validación directamente en el cliente. Usaremos Svelte para gestionar el estado de los errores.

<!-- src/routes/my-form/+page.svelte (validación cliente) -->
<script lang="ts">
  import { enhance } from '@sveltejs/kit/forms';
  export let data;
  import type { ActionData } from './$types';

  let form: ActionData = data.form;
  let isLoading = false;

  let name = form?.name || '';
  let email = form?.email || '';

  let errors: { name?: string; email?: string } = {};

  $: {
    // Reactividad para limpiar errores cuando los campos cambian
    if (errors.name && name) errors.name = undefined;
    if (errors.email && email) errors.email = undefined;
  }

  function validateClient() {
    errors = {}; // Resetear errores
    if (!name) {
      errors.name = 'El nombre es requerido.';
    }
    if (!email) {
      errors.email = 'El email es requerido.';
    } else if (!/^[^	



	 	

 	

 	

 	

 	

 	

@]+@[^	



	 	

 	

 	

 	

 	

 	

]+\.[^	



	 	

 	

 	

 	

 	

 	

]{2,}$/.test(email)) {
      errors.email = 'Introduce un email válido.';
    }

    return Object.keys(errors).length === 0;
  }

  function handleEnhanceSubmit() {
    if (!validateClient()) {
      // Si la validación del cliente falla, no enviamos el formulario
      return;
    }
    isLoading = true;
    return async ({ result, update }) => {
      if (result.type === 'error' && result.data) {
        // Si hay errores del servidor, actualizamos nuestro objeto de errores
        // Asumiendo que el servidor devuelve un objeto de errores en result.data
        errors = result.data.errors || {};
      }
      await update();
      isLoading = false;
    };
  }
</script>

<h1>Mi Formulario con Validación</h1>

<form method="POST" action="?/" use:enhance={handleEnhanceSubmit} novalidate>
  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" name="name" bind:value={name} required class={errors.name ? 'input-error' : ''}>
    {#if errors.name}
      <p class="error-message">{errors.name}</p>
    {/if}
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" bind:value={email} required class={errors.email ? 'input-error' : ''}>
    {#if errors.email}
      <p class="error-message">{errors.email}</p>
    {/if}
  </div>

  <button type="submit" disabled={isLoading || Object.keys(errors).length > 0}>
    {#if isLoading}Enviando...{:else}Enviar{/if}
  </button>
</form>

<style>
  .input-error { border-color: red; }
  .error-message { color: red; font-size: 0.8em; margin-top: 5px; }
</style>

En este ejemplo, usamos bind:value para conectar el input con las variables Svelte name y email. La declaración $: {} es reactiva: cada vez que name o email cambian, se comprueba si el error asociado debería limpiarse. El botón se deshabilita si hay errores o si está cargando.

3.2. Validación del Lado del Servidor (Zod/Valibot) 🔐

Para una validación robusta en el servidor, librerías como Zod o Valibot son excelentes opciones. Estas permiten definir esquemas de validación que pueden ser compartidos entre cliente y servidor (isomorfismo).

Instalamos Zod:

npm install zod
// src/routes/my-form/+page.server.js (con Zod)
import { fail, redirect } from '@sveltejs/kit';
import { z } from 'zod';

// Definimos el esquema de validación
const formSchema = z.object({
  name: z.string().min(1, 'El nombre es requerido.'),
  email: z.string().email('Introduce un email válido.').min(1, 'El email es requerido.'),
});

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

    try {
      const parsedData = formSchema.parse(Object.fromEntries(data));
      
      // Simular guardado en base de datos
      console.log('Datos validados y recibidos:', parsedData);

      return { success: true, message: 'Formulario enviado con éxito!' };

    } catch (err: any) {
      const errors = err.flatten ? err.flatten().fieldErrors : {};
      // Devolvemos los errores de validación
      return fail(400, { success: false, errors, message: 'Por favor, corrige los errores del formulario.' });
    }
  },
};

Ahora, el fail(400, { errors }) enviará un objeto errors al cliente, que podremos usar para mostrar los mensajes específicos. En el cliente, la función handleEnhanceSubmit debería ser capaz de recoger estos errores.

💡 Consejo: Considera crear una función `validate` genérica que use tu esquema Zod tanto en el cliente como en el servidor. Esto asegura que la lógica de validación sea consistente en ambos lados.

4. Gestión Avanzada del Estado y Feedback del Usuario 📊

Para formularios más complejos, es útil tener una gestión de estado más sofisticada. Los Svelte Stores pueden ser ideales para esto, especialmente si necesitas que el estado del formulario sea accesible desde múltiples componentes o persista entre rutas (aunque esto último es menos común para formularios).

4.1. Usando Stores para el Estado del Formulario

Crear un store para el estado del formulario permite centralizar los valores, errores y estados de carga.

// src/stores/formStore.js
import { writable } from 'svelte/store';

export const formState = writable({
  name: '',
  email: '',
  errors: {},
  isLoading: false,
  successMessage: '',
  errorMessage: '',
});

Luego, en tu componente +page.svelte:

<!-- src/routes/my-form/+page.svelte (con Store) -->
<script lang="ts">
  import { enhance } from '@sveltejs/kit/forms';
  import { formState } from '../../stores/formStore'; // Asegúrate de la ruta correcta
  import { z } from 'zod'; // Si compartes esquemas con el cliente

  // Definimos el esquema de validación (idealmente compartido)
  const clientFormSchema = z.object({
    name: z.string().min(1, 'El nombre es requerido desde el cliente.'),
    email: z.string().email('Introduce un email válido desde el cliente.').min(1, 'El email es requerido desde el cliente.'),
  });

  export let data; // Los datos del servidor aún llegan aquí inicialmente
  import type { ActionData } from './$types';

  // Actualizamos el store con los datos iniciales o del formulario devueltos por el servidor
  $: {
    if (data.form) {
      formState.update(state => ({
        ...state,
        name: data.form.name || state.name,
        email: data.form.email || state.email,
        errors: data.form.errors || {},
        successMessage: data.form.success ? data.form.message : '',
        errorMessage: !data.form.success && data.form.message ? data.form.message : '',
        isLoading: false // Resetear en cada carga/actualización
      }));
    }
  }

  // Acceder a los valores del store reactivamente
  let { name, email, errors, isLoading, successMessage, errorMessage } = $formState;

  $: {
    // Reactividad para limpiar errores cuando los campos cambian
    if (errors.name && name) $formState.update(s => ({...s, errors: {...s.errors, name: undefined}}));
    if (errors.email && email) $formState.update(s => ({...s, errors: {...s.errors, email: undefined}}));
  }

  function validateClient() {
    try {
      clientFormSchema.parse({ name, email });
      $formState.update(s => ({...s, errors: {}})); // Limpiar errores si la validación es exitosa
      return true;
    } catch (err: any) {
      const clientErrors = err.flatten ? err.flatten().fieldErrors : {};
      $formState.update(s => ({...s, errors: clientErrors, errorMessage: 'Por favor, corrige los errores del formulario.'}));
      return false;
    }
  }

  function handleEnhanceSubmit() {
    if (!validateClient()) {
      return;
    }
    $formState.update(s => ({...s, isLoading: true, successMessage: '', errorMessage: ''}));
    return async ({ result, update }) => {
      if (result.type === 'error' && result.data) {
        // Manejar errores del servidor
        $formState.update(s => ({
          ...s,
          errors: result.data.errors || {},
          errorMessage: result.data.message || 'Error al enviar el formulario.',
          successMessage: ''
        }));
      } else if (result.type === 'success' && result.data) {
        // Manejar éxito del servidor
        $formState.update(s => ({
          ...s,
          name: '', // Limpiar el formulario en éxito
          email: '',
          errors: {},
          successMessage: result.data.message || 'Formulario enviado con éxito.',
          errorMessage: ''
        }));
      }
      await update(); // SvelteKit se encarga de re-renderizar con los nuevos datos
      $formState.update(s => ({...s, isLoading: false}));
    };
  }
</script>

<h1>Mi Formulario con Store y Validación Avanzada</h1>

{#if successMessage}
  <div class="callout tip">✅ <strong>Éxito:</strong> {successMessage}</div>
{:else if errorMessage}
  <div class="callout warning">⚠️ <strong>Error:</strong> {errorMessage}</div>
{/if}

<form method="POST" action="?/" use:enhance={handleEnhanceSubmit} novalidate>
  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" name="name" bind:value={name} required class={errors.name ? 'input-error' : ''}>
    {#if errors.name}
      <p class="error-message">{errors.name}</p>
    {/if}
  </div>

  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" bind:value={email} required class={errors.email ? 'input-error' : ''}>
    {#if errors.email}
      <p class="error-message">{errors.email}</p>
    {/if}
  </div>

  <button type="submit" disabled={isLoading || Object.keys(errors).length > 0}>
    {#if isLoading}Enviando...{:else}Enviar{/if}
  </button>
</form>

<style>
  .input-error { border-color: red; }
  .error-message { color: red; font-size: 0.8em; margin-top: 5px; }
</style>

4.2. Mejorando la Accesibilidad (ARIA) ♿

Para hacer tus formularios más accesibles, es crucial usar atributos ARIA. Esto ayuda a los lectores de pantalla a entender la estructura y el estado de tus campos.

<label for="name">Nombre:</label>
<input 
  type="text" 
  id="name" 
  name="name" 
  required 
  bind:value={name} 
  aria-invalid={!!errors.name} 
  aria-describedby={errors.name ? 'name-error' : undefined}
>
{#if errors.name}
  <p id="name-error" class="error-message" role="alert">{errors.name}</p>
{/if}
  • aria-invalid="true" indica que el campo tiene un error.
  • aria-describedby="id-del-mensaje-de-error" asocia el campo de entrada con su mensaje de error, permitiendo que los lectores de pantalla anuncien el error.
  • role="alert" en el mensaje de error asegura que el lector de pantalla lo anuncie inmediatamente.
📌 Nota: Siempre valida ambos lados, cliente y servidor. La validación del cliente mejora la UX, la del servidor asegura la integridad de los datos.

5. Casos de Uso Avanzados y Patrones 🎯

5.1. Formularios Multi-paso (Multi-step Forms)

Para formularios largos, dividirlos en pasos es una excelente estrategia UX. Esto se puede lograr con SvelteKit manteniendo el estado del formulario en un store y renderizando diferentes componentes o secciones del formulario según el paso actual.

Estrategia:

  1. Estado centralizado: Usa un Svelte Store para mantener todos los datos del formulario, el paso actual y los errores.
  2. Componentes de paso: Cada paso del formulario es un componente Svelte separado.
  3. Navegación: Botones 'Siguiente' y 'Anterior' que actualizan el paso en el store.
  4. Validación por paso: Valida cada paso antes de permitir avanzar al siguiente.
  5. Envío final: Solo se realiza un envío (POST) al servidor cuando todos los pasos han sido completados y validados.
INICIO Paso 1: Datos Personales ¿VÁLIDO? NO Mostrar errores Paso 2: Info. Adicional ¿VÁLIDO? NO Mostrar errores Revisar y Enviar Acción de Servidor FIN

5.2. Subida de Archivos 📁

Los formularios de subida de archivos son un poco diferentes ya que requieren el enctype="multipart/form-data".

<form method="POST" action="?/upload" enctype="multipart/form-data" use:enhance>
  <label for="file">Subir archivo:</label>
  <input type="file" id="file" name="file" required>
  <button type="submit">Subir</button>
</form>

En +page.server.js:

// src/routes/upload/+page.server.js
import { fail } from '@sveltejs/kit';

export const actions = {
  upload: async ({ request }) => {
    const data = await request.formData();
    const file = data.get('file') as File; // 'File' es un tipo de Blob en Node.js

    if (!file || file.size === 0) {
      return fail(400, { success: false, message: 'No se ha subido ningún archivo.' });
    }

    // Aquí puedes guardar el archivo en el sistema de archivos o en un almacenamiento en la nube
    // Por ejemplo, usando 'fs' para guardarlo localmente (requiere un entorno Node.js para el build)
    // const fs = await import('node:fs/promises');
    // await fs.writeFile(`./uploads/${file.name}`, Buffer.from(await file.arrayBuffer()));
    
    console.log(`Archivo ${file.name} subido, tamaño: ${file.size} bytes.`);

    return { success: true, message: `Archivo '${file.name}' subido con éxito.` };
  }
};

5.3. Interacciones con invalidateAll y goto 🚀

Después de un envío exitoso, a menudo querrás actualizar la UI sin una recarga completa o redirigir al usuario.

  • invalidateAll(): Invalida todos los datos load en la página actual o subyacentes, forzando una re-ejecución de las funciones load y refrescando la UI con los datos más recientes. Es útil si el formulario afecta a otros datos mostrados en la página.
  • goto('/some-page'): Redirige al usuario a una nueva página.

Ambos se pueden usar en el callback de use:enhance.

<script lang="ts">
  import { enhance, applyAction } from '@sveltejs/kit/forms';
  import { invalidateAll } from '$app/navigation';
  import { goto } from '$app/navigation';
  // ... otros imports y lógica ...

  function handleEnhanceSubmit() {
    // ... validación cliente ...

    return async ({ result, update }) => {
      if (result.type === 'success') {
        // Opciones después de un éxito:
        // 1. Invalida y actualiza los datos actuales sin cambiar de página
        // await invalidateAll(); 
        
        // 2. Redirige a otra página
        await goto('/dashboard/forms/success'); 

        // 3. Simplemente actualiza el componente (esto ya lo hace 'update()')
        // await update();

      } else {
        // Si hay un error, dejamos que 'update()' se encargue de mostrar los mensajes
        await update();
      }
    };
  }
</script>

Conclusión y Mejores Prácticas ✅

La gestión de formularios en SvelteKit, combinando la mejora progresiva con las características reactivas de Svelte y las acciones de servidor, ofrece una solución potente y elegante. Al seguir estas prácticas, puedes construir formularios robustos, accesibles y fáciles de mantener.

¡Dominio Completo de Formularios!

Recapitulando los puntos clave:

  • Mejora Progresiva: Siempre empieza con un formulario HTML funcional que envíe datos al servidor, incluso sin JavaScript.
  • Acciones de Servidor: Utiliza +page.server.js para manejar los envíos POST, procesar datos y realizar validaciones críticas.
  • use:enhance: Intercepta los envíos de formulario en el cliente para una experiencia sin recargas, añadiendo interactividad y feedback instantáneo.
  • Validación Dual: Implementa validación tanto en el cliente (para UX) como en el servidor (para seguridad y fiabilidad).
  • Librerías de Validación: Herramientas como Zod o Valibot simplifican la definición de esquemas y permiten la validación isomorfa.
  • Gestión de Estado: Usa bind:value y stores de Svelte para manejar el estado de los campos, errores y estados de carga.
  • Accesibilidad (ARIA): No olvides los atributos ARIA para garantizar que tus formularios sean utilizables por todos los usuarios.
  • Feedback al Usuario: Proporciona indicadores de carga y mensajes claros de éxito o error.

Al aplicar estos principios, tus formularios en SvelteKit no solo serán funcionales, sino que también destacarán por su usabilidad, rendimiento y accesibilidad.

Tutoriales relacionados

Comentarios (0)

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