Skip to content

Instantly share code, notes, and snippets.

@sillvva
Last active December 16, 2025 21:39
Show Gist options
  • Select an option

  • Save sillvva/6b85e530d4ae74ab342293761ee4bd04 to your computer and use it in GitHub Desktop.

Select an option

Save sillvva/6b85e530d4ae74ab342293761ee4bd04 to your computer and use it in GitHub Desktop.
Remote form factory
import { beforeNavigate } from "$app/navigation";
import { page } from "$app/state";
import { debounce, deepEqual } from "@sillvva/utils";
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { RemoteForm, RemoteFormFields, RemoteFormInput, RemoteFormIssue } from "@sveltejs/kit";
import { onMount, tick, untrack } from "svelte";
import { toast } from "svelte-sonner";
import { v7 } from "uuid";
import { unknownErrorMessage } from "./util";
export type ExtractId<Input> = Input extends { id: infer Id }
? Id extends string | number
? Id
: string | number
: string | number;
export interface RemoteFormOptions<Input extends RemoteFormInput, Output> {
key?: ExtractId<Input>;
schema?: StandardSchemaV1<Input, unknown>;
data?: Input | $state.Snapshot<Input>;
initialErrors?: boolean;
resetOn?: "data" | "url";
navBlockMessage?: string;
onsubmit?: <T>(ctx: { readonly tainted: boolean; readonly form: HTMLFormElement; readonly data: Input }) => Awaitable<T>;
oninput?: (ev: Event & { currentTarget: EventTarget & HTMLFormElement }) => void;
onresult?: (ctx: {
readonly success: boolean;
readonly result?: RemoteForm<Input, Output>["result"];
readonly issues?: RemoteFormIssue[];
readonly error?: unknown;
}) => Awaitable<void>;
onissues?: (ctx: { readonly issues: RemoteFormIssue[] }) => Awaitable<void>;
}
export interface ConfiguredForm<Input extends RemoteFormInput, Output> {
(): {
form: Omit<RemoteForm<Input, Output>, "for">;
attrs: {
[attachment: symbol]: (node: HTMLFormElement) => void;
method: "POST";
action: string;
onsubmit: (
ev: Event & {
currentTarget: EventTarget & HTMLFormElement;
}
) => Promise<void>;
oninput: (
ev: Event & {
currentTarget: EventTarget & HTMLFormElement;
}
) => void;
};
result: Output | undefined;
dirty: boolean;
submitting: boolean;
};
}
export function configureForm<Input extends RemoteFormInput, Output>(
form: RemoteForm<Input, Output>,
options = () => ({}) as RemoteFormOptions<Input, Output>
): ConfiguredForm<Input, Output> {
let configuredForm: Omit<RemoteForm<Input, Output>, "for"> = $state.raw(form);
let initial = $state.raw($state.snapshot(options().data));
let submitting = $state.raw(false);
type Fields = RemoteFormFields<unknown>;
function setup(config: RemoteFormOptions<Input, Output>) {
initial = $state.snapshot(config?.data);
configuredForm = form.for((config.key ?? config.data?.id ?? v7()) as ExtractId<Input>);
if (config?.schema) {
configuredForm = configuredForm.preflight(config.schema as unknown as StandardSchemaV1<Input, unknown>);
}
if (config.data) {
(configuredForm.fields as Fields).set(config.data);
}
}
setup(options());
let hydrated = false;
$effect(() => {
const config = untrack(options);
const updateOn = config.resetOn ?? (untrack(() => config.data) ? "data" : "url");
if (updateOn === "data") void options().data;
else void page.url;
if (!hydrated) return void (hydrated = true);
untrack(() => setup(config));
});
let dirty = $derived(!deepEqual(initial, $state.snapshot(configuredForm.fields.value())));
const result = $derived(configuredForm.result);
const issues = $derived(configuredForm.fields.issues());
const allIssues = () => (configuredForm.fields as Fields).allIssues();
let lastIssues = $state.raw<RemoteFormIssue[] | undefined>(allIssues());
const debouncedValidate = debounce(validate, 300);
async function validate() {
const config = options();
await configuredForm.validate({ includeUntouched: true, preflightOnly: true });
const issues = allIssues();
if (issues && config?.onissues && !deepEqual(lastIssues, issues)) config.onissues({ issues });
if (issues?.length) lastIssues = issues;
}
async function focusInvalid(formEl: HTMLFormElement) {
await tick();
const issues = allIssues();
if (issues?.length) lastIssues = issues;
else return;
const invalid = formEl.querySelector(":is(input, select, textarea):not(.hidden, [type=hidden], :disabled)[aria-invalid]") as
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement
| null;
invalid?.focus();
}
onMount(() => {
const config = options();
if (config?.initialErrors) validate();
});
beforeNavigate((ev) => {
const config = options();
if (!config?.navBlockMessage) return;
if ((dirty || issues) && !confirm(config.navBlockMessage)) {
return ev.cancel();
}
});
return () => ({
form: configuredForm,
attrs: {
...configuredForm.enhance(async ({ submit, form: formEl, data }) => {
const config = options();
const bf = !config.onsubmit || (await config.onsubmit({ tainted: dirty, form: formEl, data }));
if (!bf) return;
submitting = true;
const wasDirty = dirty;
try {
dirty = false;
await submit();
const issues = allIssues();
const success = !issues?.length;
config.onresult?.({ success, result: configuredForm.result, issues });
if (success) {
successToast(`${(configuredForm.fields as Fields).name?.value() || "Form"} saved successfully`);
} else {
dirty = wasDirty;
await focusInvalid(formEl);
config.onissues?.({ issues });
}
} catch (error) {
unknownErrorToast(error || "Oh no! Something went wrong");
config.onresult?.({ success: false, error });
dirty = wasDirty;
} finally {
submitting = false;
}
}),
onsubmit: (ev) => focusInvalid(ev.currentTarget),
oninput: (ev) => {
const config = options();
if (lastIssues) debouncedValidate.call();
config.oninput?.(ev);
}
},
initial,
result,
dirty,
submitting
});
}
export function successToast(message: string) {
toast.success("Success", {
description: message,
classes: {
description: "text-white!"
}
});
}
export function errorToast(message: string) {
toast.error("Error", {
description: message,
classes: {
description: "text-white!"
},
duration: Duration.toMillis("30 seconds")
});
}
export function unknownErrorToast(error: unknown) {
errorToast(unknownErrorMessage(error));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment