Skip to content

Instantly share code, notes, and snippets.

@sillvva
Last active October 1, 2025 07:52
Show Gist options
  • Save sillvva/6515b65d85075133de7c2e533b730e98 to your computer and use it in GitHub Desktop.
Save sillvva/6515b65d85075133de7c2e533b730e98 to your computer and use it in GitHub Desktop.
Remote Functions + Superforms
<script lang="ts">
import Breadcrumbs from "$lib/components/Breadcrumbs.svelte";
import Control from "$lib/components/forms/Control.svelte";
import Input from "$lib/components/forms/Input.svelte";
import Submit from "$lib/components/forms/Submit.svelte";
import SuperForm from "$lib/components/forms/SuperForm.svelte";
import { valibotForm } from "$lib/factories.svelte.js";
import DMsAPI from "$lib/remote/dms";
import { dungeonMasterSchema } from "$lib/schemas";
let { data } = $props();
// Invalidates all on redirect
const superform = valibotForm(data.form, dungeonMasterSchema, {
remote: DMsAPI.forms.save
});
// Alternatively with single-flight mutations:
// const superform = valibotForm(data.form, dungeonMasterSchema, {
// remote: (data) => DMsAPI.forms.save(data).updates(getDMs(data.user.id), getDM(data.dm.id)),
// });
// const superform = valibotForm(data.form, dungeonMasterSchema, {
// remote: DMsAPI.forms.save,
// remoteUpdates: () => ([getDMs(data.user.id), getDM(data.dm.id)])
// });
</script>
<div class="flex flex-col gap-4">
<Breadcrumbs />
<SuperForm {superform}>
<Control class="col-span-12 sm:col-span-6">
<Input type="text" {superform} field="name" label="DM Name" />
</Control>
<Control class="col-span-12 sm:col-span-6">
<Input type="text" {superform} field="DCI" label="DCI" />
</Control>
<Submit {superform}>Save DM</Submit>
</SuperForm>
</div>
// Success Result
export type EffectSuccess<R> = { ok: true; data: R };
// Failure Result
export type EffectFailure = {
ok: false;
error: { message: string; status: NumericRange<300, 599>; [key: string]: unknown };
};
// Discriminating Union
export type EffectResult<R> = EffectSuccess<R> | EffectFailure;
// Pathname comes from @sveltejs/kit.
export type FullPathname = Pathname | `${Pathname}?${string}`;
// Single Flight Mutation Refreshes
type RemoteUpdates = Array<RemoteQuery<unknown> | RemoteQueryOverride>;
/**
* Configuration options for custom form behavior when using remote functions.
*/
export interface CustomFormOptions<Out extends Record<string, unknown>> {
/**
* Remote function handler that processes form data and returns validation results or redirect paths.
* When provided, the form will use this instead of the default SvelteKit form actions.
*/
remote?: (data: Out) => Promise<EffectResult<SuperValidated<Out> | FullPathname>>;
/**
* Must be undefined for CustomFormOptions. Use OptionsWithUpdates if you need remote updates.
*/
remoteUpdates?: undefined;
/**
* Callback executed when form submission succeeds. Receives the validated form data.
* @param data - The successfully validated form data
*/
onSuccessResult?: (data: Out) => Awaitable<void>;
/**
* Callback executed when form submission fails. Receives the error details.
* @param error - The error that occurred during submission
*/
onErrorResult?: (error: EffectFailure["error"]) => Awaitable<void>;
}
/**
* Configuration options for forms that need to update remote data after successful submission.
* Extends CustomFormOptions but requires remote updates functionality.
*/
export interface OptionsWithUpdates<Out extends Record<string, unknown>> extends Omit<CustomFormOptions<Out>, "remoteUpdates"> {
/**
* Remote function handler with updates capability. Must include an `updates` method
* that can invalidate specific remote queries after successful form submission.
*/
remote?: (data: Out) => Promise<EffectResult<SuperValidated<Out> | FullPathname>> & {
updates: (...queries: RemoteUpdates) => Promise<EffectResult<SuperValidated<Out> | FullPathname>>;
};
/**
* Array of remote queries to invalidate after successful form submission.
*/
remoteUpdates: () => RemoteUpdates;
}
/**
* Creates a SvelteKit Superforms form with Valibot validation and enhanced remote function capabilities.
*
* This function combines the power of SvelteKit Superforms with Valibot schema validation, providing
* a type-safe form solution that supports both traditional form submission and remote functions.
* It includes built-in error handling, success notifications, and optional query invalidation.
*
* @param form - The SuperValidated form data from SvelteKit Superforms, typically from a load function
* @param schema - The Valibot schema used for client-side validation
* @param options - Configuration options for form behavior, validation, and submission handling
*
* @returns Enhanced SuperForm object with additional properties:
* - All standard SvelteKit Superforms properties (form, errors, message, etc.)
* - `pending` - Writable store indicating if form submission is in progress
* - `submitCount` - Writable store tracking the number of submission attempts
*
* @example
* ```typescript
* // Basic usage with Valibot schema
* const schema = v.object({
* name: v.string(),
* email: v.pipe(v.string(), v.email())
* });
*
* const { form, errors, enhance } = $derived(valibotForm(data.form, schema));
*
* // Usage in Svelte component
* <form method="POST" use:enhance>
* <input bind:value={$form.name} />
* {#if $errors.name}
* <span class="error">{$errors.name}</span>
* {/if}
* </form>
* ```
*
* @example
* ```typescript
* // Advanced usage with remote functions and updates
* const { form, errors, enhance, pending } = $derived(valibotForm(data.form, schema, {
* remote: updateCharacter,
* remoteUpdates: [
* getCharacters(user.id),
* getCharacter(character.id)
* ]
* }));
* ```
*
* @see {@link https://superforms.rocks/ SvelteKit Superforms Documentation}
* @see {@link https://valibot.dev/ Valibot Documentation}
*/
export function valibotForm<S extends v.GenericSchema, Out extends Infer<S, "valibot">, In extends InferIn<S, "valibot">>(
form: SuperValidated<Out, App.Superforms.Message, In>,
schema: S,
{
remote,
remoteUpdates,
invalidateAll: inalidate,
onSuccessResult = (data) => (typeof data === "object" && "name" in data ? successToast(`${data.name} saved`) : undefined),
onErrorResult = (error) => errorToast(error.message),
onSubmit,
onResult,
onError,
...rest
}: FormOptions<Out, App.Superforms.Message, In> & (CustomFormOptions<Out> | OptionsWithUpdates<Out>) = {}
) {
// Create reactive stores for form state management
const pending = writable(false);
const submitCount = writable(0);
// Initialize the SvelteKit Superforms instance with Valibot validation
const superform = superForm(form, {
dataType: "json", // Use JSON for form data transmission
validators: valibotClient(schema), // Integrate Valibot schema for client-side validation
taintedMessage: "You have unsaved changes. Are you sure you want to leave?",
...rest, // Spread any additional Superforms options
// Custom submit handler that supports both traditional and remote function
onSubmit: async (event) => {
// Update form state to indicate submission is in progress
pending.set(true);
submitCount.update((count) => count + 1);
// Execute custom onSubmit callback if provided
// If the callback returns false, cancel the submission
if ((await onSubmit?.(event)) === false) {
pending.set(false);
return event.cancel();
}
// Handle remote function if a remote handler is provided
if (remote) {
// Cancel the default form submission since we're handling it remotely
event.cancel();
// Get current form data and execute remote function
const data = get(superform.form);
const result = (remoteUpdates && (await remote(data).updates(...remoteUpdates()))) ?? (await remote(data));
if (result.ok) {
// Handle successful remote function submission
if (typeof result.data === "string") {
// String result indicates a redirect path (FullPathname)
superform.tainted.set(undefined); // Clear tainted state
await onSuccessResult(data); // Execute success callback
await goto(result.data); // Navigate to the specified path
return;
}
// Handle validation result from remote function
const hasErrors = Object.keys(result.data.errors).length > 0;
superform.errors.set(result.data.errors); // Update form errors
superform.message.set(result.data.message); // Update form message
superform.form.set(result.data.data, {
taint: hasErrors ? true : "untaint-form" // Manage form taint state
});
// Execute success callback only if there are no validation errors
if (!hasErrors) await onSuccessResult(data);
} else {
// Handle remote function failure
await onErrorResult(result.error); // Execute error callback
if (isRedirectFailure(result.error)) {
// Handle redirect failure (e.g., authentication required)
superform.tainted.set(undefined); // Clear tainted state
await goto(result.error.redirectTo); // Navigate to redirect path
return;
} else {
// Handle other types of errors
const error = result.error.message;
superform.errors.set({ _errors: [error] }); // Set form-level error
}
}
pending.set(false); // Clear pending state
}
},
// Handle form result events (success, failure, redirect)
onResult(event) {
// Clear pending state for non-redirect results
if (event.result.type !== "redirect") pending.set(false);
// Execute success callback for successful submissions or redirects
if (["success", "redirect"].includes(event.result.type)) {
onSuccessResult(get(superform.form));
}
// Execute custom onResult callback if provided
onResult?.(event);
}
});
// Cleanup: invalidate all data when component is destroyed (if conditions are met)
onDestroy(async () => {
if (get(submitCount) > 0 && inalidate !== false && !remoteUpdates?.length) {
await invalidateAll();
}
});
// Return enhanced SuperForm with additional state management
return {
...superform, // Spread all standard Superforms properties
pending, // Reactive store for submission state
submitCount // Reactive store for submission attempt count
};
}
// $lib/remotes/dms/forms.remote.ts
import { command } from "$app/server";
import { dungeonMasterIdSchema, dungeonMasterSchema, type DungeonMasterSchemaIn } from "$lib/schemas";
import { redirectOnFail } from "$lib/server/effect/errors";
import { parse, saveForm, validateForm } from "$lib/server/effect/forms";
import { runSafe } from "$lib/server/effect/runtime";
import { assertAuth } from "$lib/server/effect/services/auth";
import { DMService } from "$lib/server/effect/services/dms";
// Skip built-in validation
export const save = command("unchecked", (input: DungeonMasterSchemaIn) =>
// returns EffectResult type (see factories.svelte.ts)
runSafe(function* () {
const { user } = yield* assertAuth();
const DMs = yield* DMService;
const dmId = yield* redirectOnFail(parse(dungeonMasterIdSchema, input.id), "/dms", 301);
// Superforms validation
const form = yield* validateForm(input, dungeonMasterSchema);
if (!form.valid) return form;
// Returns form or redirect path
return yield* saveForm(DMs.set.save(dmId, user, form.data), {
onSuccess: () => "/dms",
onError: (err) => {
err.toForm(form);
return form;
}
});
})
);
<script lang="ts">
import { dev } from "$app/environment";
import { onMount, type Snippet } from "svelte";
import { fromAction } from "svelte/attachments";
import type { HTMLFormAttributes } from "svelte/elements";
import type { Writable } from "svelte/store";
import type { SuperForm } from "sveltekit-superforms";
import SuperDebug from "sveltekit-superforms/SuperDebug.svelte";
import FormMessage from "./FormMessage.svelte";
type FormAttributes = Omit<HTMLFormAttributes, "hidden">;
type T = $$Generic<Record<PropertyKey, unknown>>;
interface Props extends Omit<FormAttributes, "action"> {
superform: SuperForm<T, App.Superforms.Message> & { pending: Writable<boolean> };
children?: Snippet;
}
let { superform, children, ...rest }: Props = $props();
const { form, errors, message, enhance, capture, restore, submitting, tainted, pending } = superform;
const isSubmitting = $derived($submitting || $pending);
onMount(() => {
superform.reset();
});
export const snapshot = {
capture,
restore
};
</script>
<div class="flex flex-col gap-4">
<FormMessage {message} />
{#if $errors._errors?.[0]}
<div class="alert alert-error shadow-lg">
<span class="iconify mdi--alert-circle size-6"></span>
{$errors._errors[0]}
</div>
{/if}
<form {...rest} method="post" {@attach fromAction(enhance)}>
<fieldset class="grid grid-cols-12 gap-4" disabled={isSubmitting}>
{@render children?.()}
</fieldset>
</form>
{#if dev}
<SuperDebug data={{ $form, $errors, $message, isSubmitting, $tainted }} />
{/if}
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment