Handle form submissions with progressive enhancement, server-side validation, and client-side form handling utilities.
// Client-side form utilities
import { enhance, applyAction, deserialize } from '$app/forms';
import type { SubmitFunction, ActionResult } from '$app/forms';
// Server-side actions
import type { Actions, Action, ActionFailure } from '@sveltejs/kit';
import { fail, error, redirect } from '@sveltejs/kit';Define server-side form handlers in +page.server.ts files.
/**
* Single form action that processes form submissions
*/
type Action<
Params extends Partial<Record<string, string>> = Partial<Record<string, string>>,
OutputData extends Record<string, any> | void = Record<string, any> | void,
RouteId extends string | null = string | null
> = (event: RequestEvent<Params, RouteId>) => OutputData | Promise<OutputData>;
/**
* Multiple named form actions
*/
type Actions<
Params extends Partial<Record<string, string>> = Partial<Record<string, string>>,
OutputData extends Record<string, any> | void = Record<string, any> | void,
RouteId extends string | null = string | null
> = Record<string, Action<Params, OutputData, RouteId>>;Usage Examples:
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email) {
return fail(400, { email, missing: true });
}
const user = await authenticate(email, password);
if (!user) {
return fail(401, { email, incorrect: true });
}
cookies.set('session', user.sessionId, { path: '/' });
redirect(303, '/dashboard');
}
} satisfies Actions;
// Multiple named actions
export const actions = {
login: async ({ request }) => {
// Handle login
},
register: async ({ request }) => {
// Handle registration
},
resetPassword: async ({ request }) => {
// Handle password reset
}
} satisfies Actions;Enhance forms with client-side handling for better UX.
/**
* Progressively enhance a form element
* @param form_element - The form element to enhance
* @param submit - Optional callback for custom handling
* @returns Cleanup function
*/
function enhance<
Success extends Record<string, any> = Record<string, any>,
Failure extends Record<string, any> = Record<string, any>
>(
form_element: HTMLFormElement,
submit?: SubmitFunction<Success, Failure>
): { destroy(): void };
type SubmitFunction<Success = Record<string, any>, Failure = Record<string, any>> = (input: {
action: URL;
formData: FormData;
formElement: HTMLFormElement;
controller: AbortController;
submitter: HTMLElement | null;
cancel(): void;
}) => void | ((opts: {
formData: FormData;
formElement: HTMLFormElement;
action: URL;
result: ActionResult<Success, Failure>;
update: (options?: { reset?: boolean; invalidateAll?: boolean }) => Promise<void>;
}) => void | Promise<void>);Usage Examples:
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<!-- Basic enhancement -->
<form method="POST" use:enhance>
<input name="email" type="email" />
<button>Submit</button>
</form>
<!-- Custom submit handling -->
<form
method="POST"
use:enhance={({ formData, cancel }) => {
// Modify form data before submit
formData.append('timestamp', Date.now().toString());
// Conditionally cancel
if (!confirm('Submit?')) {
cancel();
}
// Return update function
return async ({ result, update }) => {
if (result.type === 'success') {
console.log('Success!', result.data);
}
await update(); // Apply default behavior
};
}}
>
<input name="email" />
<button>Submit</button>
</form>
<!-- Access form validation result -->
{#if form?.missing}
<p>Email is required</p>
{/if}
{#if form?.incorrect}
<p>Invalid credentials</p>
{/if}Manually apply action results to update page state.
/**
* Apply an action result to update form and page data
* @param result - The action result to apply
* @returns Promise that resolves when applied
*/
function applyAction<
Success extends Record<string, any> = Record<string, any>,
Failure extends Record<string, any> = Record<string, any>
>(result: ActionResult<Success, Failure>): Promise<void>;Usage Example:
<script lang="ts">
import { applyAction } from '$app/forms';
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData
});
const result = await response.json();
await applyAction(result);
}
</script>
<form on:submit={handleSubmit}>
<!-- form fields -->
</form>Deserialize action result from response.
/**
* Deserialize action result from fetch response
* @param result - Serialized action result string
* @returns Deserialized ActionResult
*/
function deserialize<
Success extends Record<string, any> = Record<string, any>,
Failure extends Record<string, any> = Record<string, any>
>(result: string): ActionResult<Success, Failure>;Usage Example:
import { deserialize } from '$app/forms';
const response = await fetch('/form-action', {
method: 'POST',
body: formData
});
const text = await response.text();
const result = deserialize(text);
if (result.type === 'success') {
console.log('Success data:', result.data);
}type ActionResult<
Success extends Record<string, any> = Record<string, any>,
Failure extends Record<string, any> = Record<string, any>
> =
| { type: 'success'; status: number; data?: Success }
| { type: 'failure'; status: number; data?: Failure }
| { type: 'redirect'; status: number; location: string }
| { type: 'error'; status?: number; error: any };
interface ActionFailure<T extends Record<string, any> | undefined = Record<string, any> | undefined> {
status: number;
data: T;
}export const actions = { default: async () => {} }?/actionName in form action attributeenhance() adds:
fail(): Return validation errors (status 4xx)redirect(): Navigate after successform prop (ActionData type)formAction prop to specify which action to call: <form action="?/register">