or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

forms.mddocs/reference/

Form Actions and Enhancement

Handle form submissions with progressive enhancement, server-side validation, and client-side form handling utilities.

Core Imports

// 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';

Capabilities

Form Actions

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;

Progressive Form Enhancement

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}

Apply Action Result

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

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);
}

Types

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;
}

Notes

  • Actions are defined in +page.server.ts or +layout.server.ts files
  • Default action: export const actions = { default: async () => {} }
  • Named actions: Call with ?/actionName in form action attribute
  • Forms work without JavaScript (progressive enhancement)
  • enhance() adds:
    • Client-side submission
    • Optimistic UI updates
    • No full page reload
    • Error handling
  • Action return values:
    • fail(): Return validation errors (status 4xx)
    • redirect(): Navigate after success
    • Plain object: Success data
  • Form data in page component available as form prop (ActionData type)
  • After action runs, form value is populated with returned data
  • Use formAction prop to specify which action to call: <form action="?/register">