form
La función form facilita escribir datos hacia el servidor. Recibe una función callback que recibe datos construidos a partir del FormData enviado.
Cuando se envía un formulario HTML, los datos viajan en un objeto especial llamado FormData, que funciona como un contenedor con los pares nombre-valor de cada campo del formulario (por ejemplo: title: "Mi post", content: "Lorem ipsum").
La función form toma esos datos crudos del FormData y los convierte en un objeto de JavaScript normal y bien tipado, que luego se pasa directamente al callback.
Sin form, sería necesario extraer cada valor manualmente del FormData. Con form, los valores llegan ya listos para usarse dentro del callback.
import * as v from "valibot";
import { error, redirect } from "@sveltejs/kit";
import { query, form } from "$app/server";
import * as db from "$lib/server/database";
import * as auth from "$lib/server/auth";
export const getPosts = query( async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(
v.object({
title: v.pipe(v.string(), v.notEmpty()),
content: v.pipe(v.string(), v.notEmpty())
}),
async ({ title, content }) => {
// Check the user is logged in
const user = await auth.getUser();
if(!user) error(401, "Unauthorized");
const slug = title.toLowerCase().replace(/ /g, '-');
// Insert into the database
await db.sql`
INSERT INTO post (slug, title, content)
VALUES (${slug}, ${title}, ${content})
`;
// Redirect to the newly created page
redirect(303, '/blog/${slug}`);
}
);
- ... y devuelve un objeto que puede ser esparcido (spread) sobre un elemento
<form> - La función callback —definida dentro del archivo
.remote.ts— se ejecuta en el servidor cada vez que el formulario se envía
<script>
import { createPost } from "../data.remote";
</script>
<h1>Create a new post</h1>
<form {...createPost}>
<!-- form content goes here -->
<button>Publish!</button>
</form>
El objeto form (el que retorna form(...), ej. createPost) contiene las propiedades method y action, que le permiten funcionar sin JavaScript (es decir, envía los datos y recarga la página). También cuenta con un attachment que mejora progresivamente el formulario cuando JavaScript está disponible, enviando los datos sin recargar toda la página.
Al igual que con query, si la función callback usa los data enviados, estos deben ser validados pasando un Standard Schema como primer argumento de form.
methodyaction: son atributos HTML normales de un<form>(por ejemplomethod="POST"yaction="/algo"). ComocreatePostya los incluye, al escribir<form {...createPost}>, el formulario queda funcional aunque el navegador no tenga JavaScript activo: se comporta como un formulario HTML clásico, enviando los datos al servidor y recargando la página con el resultado.- El "attachment" (mejora progresiva): si JavaScript está disponible, este mismo objeto
createPostañade un comportamiento extra que intercepta el envío del formulario, lo manda al servidor confetch(sin recargar la página), y actualiza solo lo necesario. El mismo<form {...createPost}>funciona en ambos escenarios (con o sin JS), usando automáticamente la mejor opción disponible. - Sobre la validación: si dentro de la función callback (la definida en
.remote.ts) se van a usar los datos que llegan del formulario (data), es necesario validarlos primero. Esto se hace pasando un Standard Schema (por ejemplo, uno creado con Valibot o Zod) como primer argumento deform(...), antes de la función callback:
form(schema, async (data) => { ... })
Esto asegura que los datos tengan el tipo y formato correctos antes de que la lógica dentro del callback los use.
Notas: disponibilidad de JavaScript y mejora progresiva
A diferencia de directivas explícitas como client: en Astro o prerender = true/false en SvelteKit —donde el desarrollador decide de antemano si algo tendrá JavaScript o cómo se genera—, la disponibilidad de JavaScript en el navegador del usuario no se decide ni se declara explícitamente. Depende del entorno de quien usa la página: si tiene JS deshabilitado, si aún no terminó de cargar/hidratarse, si falla por algún error, etc.
Por esto, el objeto form no verifica con una condición si JS está o no disponible. En su lugar, usa un attachment que se activa solo si JavaScript efectivamente llega a ejecutarse en el navegador. Si esto ocurre, el attachment intercepta el envío del formulario y lo maneja de forma mejorada (sin recargar la página). Si no ocurre, el formulario simplemente usa su comportamiento HTML nativo: los atributos method y action para enviar los datos de forma tradicional (con recarga de página).
En resumen, no existe una bifurcación tipo if/else decidida por el desarrollador: hay un comportamiento base (HTML nativo, siempre presente como respaldo) que se mejora automáticamente cuando JavaScript está disponible. Esta técnica se conoce como "mejora progresiva" (progressive enhancement): el HTML funciona por sí solo, y JS lo mejora si está disponible, sin que nadie tenga que decidirlo explícitamente.
Campos (fields)
- Un formulario se compone de un conjunto de campos definidos por el schema
- En el ejemplo anterior
createPosttiene dos campos:titleycontent, ambos de tipo string - Para obtener los atributos de un campo se llama a su método
.as(...), especificando qué tipo de input usar (email, file, password, etc.). Para la mayoría de los tipos de input, también se puede pasar un segundo argumentoas.(type, value)para controlar el valor que se renderiza
<form {...createPost}>
<label>
<h2>Title</h2>
<input {...createPost.fields.title.as('text')} />
</label>
<label>
<h2>Write your post</h2>
<textarea {...createPost.fields.content.as('text')} ></textarea>
</label>
<button>Publish!</button>
</form>
Estos atributos permiten a SvelteKit establecer el tipo de input correcto
- Establecer el
nameque se usa para construir eldataque se envía al handler - Poblar el
valuedel formulario (por ejemplo, después de un envío fallido, para evitar que el usuario tenga que volver a llenar todo) - Establecer el estado
aria-invalid
Pasar un segundo argumento a .as(...) es útil al renderizar un formulario a partir de datos existentes, como un formulario de edición o múltiples instancias creadas con for(...). Además de establecer el valor del elemento cuando se renderiza, este argumento controla el valor del elemento cuando el formulario se reinicia (reset).
Los campos pueden anidarse en objetos y arrays, y sus valores pueden ser strings, números, booleanos u objetos File.
const datingProfile = v.objetc({
name: v.string(),
photo: v.file(),
info: v.object({
height: v.number(),
likesDogs: v.optional(v.boolean(), false)
}),
attributes: v.array(v.string())
});
export const createProfile = form(datingProfile, (data) => { /* ... */ });
<script>
import { createProfile } from "./data.remote";
const { name, photo, info, attributes } = createProfile.fields;
</script>
<form {...createProfile} enctype="multipart/form-data">
<label>
<input {...name.as("text")} /> Name
</label>
<label>
<input {...photo.as("file")} /> Photo
</label>
<label>
<input {...info.height.as("number")} /> Height (cm)
</label>
<label>
<input {...info.likesDogs.as("checkbox")} /> I like dogs
</label>
<h2>My best attributes</h2>
<input {...attributes[0].as("text")} />
<input {...attributes[1].as("text")} />
<input {...attributes[2].as("text")} />
<button>Submit</button>
</form>
- Debido a que el formulario contiene un input de tipo
File, se agrega el atributoenctype="multipart/form-data" - Los valores de
info.heighteinfo.likesDogsse convierten (coerce) a número y booleano - Si un input de tipo
checkboxno está marcado, su valor no se incluye en el objetoFormDataa partir del cual SvelteKit construye los datos. Por esta razón, es necesario hacer que el valor sea opcional en el schema. Envalibotesto significa usarv.optional(v.boolean(), false)en lugar de solov.boolean(), mientras que en Zod significa usarz.coerce.boolean(<boolean>)()
En el caso de los inputs radio y chechbox que pertenecen todos al mismo campo, el value debe especificarse como segundo argumento de .as(...).
export const operatingSystems = ["windows", "mac", "linux"] as const;
export const languages = ["html", "css", "js"] as const;
import {operatingSystems, languages } from "./constants";
export const survey = form(
v.objetc({
operatingSystem: v.picklist(operatingSystems),
languages: v.optional(v.array(v.picklist(languages)), [])
}),
(data) => { /* ... */ }
);
<form {...survey}>
<h2>Wich operating system do you use?</h2>
{#each operatingSystems as os}
<label>
<input {...survey.fields.operatingSystem.as("radio", os)} />{os}
</label>
{/each}
<h2>Wich languages do you write code in?</h2>
{#each languages as language}
<label>
<input {...survey.fields.languages.as("checkbox", language)} />
{language}
</label>
{/each}
<button>Submit</button>
</form>
Como alternativa se pueden usar los select y select multiple.
- Al igual que con los inputs checkbox no marcados, si no se hace ninguna selección, el dato será
undefined. Por esta razón, el campolanguagesusav.optional(v.array(...), [ ])en lugar de solov.arra(...)
<form {...survey} >
<h2>Wich operating system do yo use?</h2>
<select {...survey.fields.operatingSystem.as("select")} >
{#each operatingSystems as os}
<option>{os}</option>
{/each}
</select>
<h2>Wich languages do you write code in?</h2>
<select>
{#each languages as language}
<option>{langugage}</option>
{/each}
</select>
<button>Submit</button>
</form>