From 84c177f67ccdd54a4f7ded7517db26dca6bdcc09 Mon Sep 17 00:00:00 2001 From: Gautier Ben Aim Date: Sat, 21 Dec 2024 20:41:40 +0100 Subject: [PATCH 1/2] feat: added reportValidity() function for sveltekit --- .../src/routes/register/+page.server.ts | 18 +++ .../src/routes/register/+page.svelte | 30 ++++ src/sveltekit/index.ts | 130 ++++++++++++++---- 3 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 demos/sveltekit/src/routes/register/+page.server.ts create mode 100644 demos/sveltekit/src/routes/register/+page.svelte diff --git a/demos/sveltekit/src/routes/register/+page.server.ts b/demos/sveltekit/src/routes/register/+page.server.ts new file mode 100644 index 0000000..2beea5e --- /dev/null +++ b/demos/sveltekit/src/routes/register/+page.server.ts @@ -0,0 +1,18 @@ +import { formfail, formgate } from "formgator/sveltekit"; +import * as fg from "formgator"; + +export const actions = { + default: formgate( + { + email: fg.email({ required: true }), + password: fg.password({ required: true }), + }, + async ({ email }) => { + if (email === "alice@example.com") { + formfail({ email: "Account already exists" }); + } + + return { success: true as const, message: "Account created" }; + }, + ), +}; diff --git a/demos/sveltekit/src/routes/register/+page.svelte b/demos/sveltekit/src/routes/register/+page.svelte new file mode 100644 index 0000000..bbadeb3 --- /dev/null +++ b/demos/sveltekit/src/routes/register/+page.svelte @@ -0,0 +1,30 @@ + + +{#if form?.success} +

+ {form.message} +

+{/if} + +
+

+ +

+

+ +

+

+ +

+
diff --git a/src/sveltekit/index.ts b/src/sveltekit/index.ts index 3eeab43..256ca2e 100644 --- a/src/sveltekit/index.ts +++ b/src/sveltekit/index.ts @@ -1,6 +1,67 @@ -import { error, fail } from "@sveltejs/kit"; +import { error, fail, type SubmitFunction } from "@sveltejs/kit"; import * as fg from "../index.js"; +class FormError extends Error { + constructor(public issues: Record) { + super("Form validation failed"); + } +} + +/** + * Throws an error that should be treated as a validation error. This allows refining the validation process inside the action function. + * + * Usage: `formfail({ email: "Account already exists" })` + * + * @example + * ```ts + * export const actions = { + * register: formgate( + * { + * email: fg.email({ required: true }), + * password: fg.password({ required: true }), + * }, + * async ({ data }) => { + * if (await userExists(data.email)) + * formfail({ email: "Account already exists" }); + * + * // ... + * } + * ) + * } + * ``` + */ +export function formfail(issues: Record): never { + throw new FormError(issues); +} + +/** + * Reports validation issues to the user interface using the `setCustomValidity` API. + * + * Usage: `
` + */ +export function reportValidity(): ReturnType { + return ({ update, result, formElement }) => { + update(); + + if (result.type === "failure" && typeof result.data?.issues === "object") { + const issues = result.data.issues as Record; + + for (const element of formElement.elements) { + if (!(element instanceof HTMLInputElement)) continue; + + // If an issue exists for the element, set the custom validity, + // otherwise, clear the custom validity with an empty string + const issue = issues[element.name]?.message ?? ""; + element.setCustomValidity(issue); + element.reportValidity(); + + // Clear the custom validity when the user interacts with the element + element.addEventListener("input", () => element.setCustomValidity(""), { once: true }); + } + } + }; +} + /** * Adds request validation to a [form * action](https://kit.svelte.dev/docs/form-actions). @@ -13,41 +74,38 @@ import * as fg from "../index.js"; * email: fg.email({ required: true }), * password: fg.password({ required: true }), * }, - * ({ data }) => { + * (data) => { * // data.email and data.password are guaranteed to be strings * // The form will be rejected as 400 Bad Request if they are missing or empty * } * ) * } - * ```; + * ``` */ export function formgate< Action, + Output, Inputs extends Record, ID extends string = string, >( inputs: Inputs, - action: Action extends (event: infer Event extends { request: Request; url: URL }) => infer Output - ? (data: fg.Infer, event: Event) => Output - : never, + action: ( + data: fg.Infer, + event: Action extends (event: infer Event) => unknown ? Event : never, + ) => Output, options: { - /** @default "POST" */ - method?: "GET" | "POST"; id?: ID; } = {}, -): Action & - (() => Promise<{ - id: ID; - success: false; - issues: fg.InferError; - accepted: Partial>; - }>) { +): () => + | Awaited + | { + id: ID; + success: false; + issues: fg.InferError; + accepted: Partial>; + } { return (async (event: { request: Request; url: URL }) => { - const data = fg - .form(inputs) - .safeParse( - options.method === "GET" ? event.url.searchParams : await event.request.formData(), - ); + const data = fg.form(inputs).safeParse(await event.request.formData()); if (!data.success) { return fail(400, { @@ -57,7 +115,29 @@ export function formgate< }); } - return action(data.data, event); + try { + return await action(data.data, event as never); + } catch (error) { + if (error instanceof FormError) { + return fail(400, { + id: options.id ?? "default", + success: false, + // Process incoming issues to mark them as "custom" issues + issues: Object.fromEntries( + Object.entries(error.issues).map(([name, message]) => [ + name, + { code: "custom", message } satisfies fg.ValidationIssue, + ]), + ), + // Remove values that were rejected + accepted: Object.fromEntries( + Object.entries(data.data).filter(([name]) => !(name in error.issues)), + ), + }); + } + + throw error; + } }) as never; } @@ -88,12 +168,8 @@ export function loadgate { const data = fg.form(inputs).safeParse(event.url.searchParams); - if (!data.success) { - return error( - 400, - `Fields ${Object.keys(data.error.issues).join(", ")} contain invalid values`, - ); - } + if (!data.success) + error(400, `Fields ${Object.keys(data.error.issues).join(", ")} contain invalid values`); return load(data.data, event as never); }) as never; From 861e085a739e4d4b33f88de1d6cf89c0a84abc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?= <48261497+GauBen@users.noreply.github.com> Date: Sat, 21 Dec 2024 20:45:36 +0100 Subject: [PATCH 2/2] Create dirty-boats-hang.md --- .changeset/dirty-boats-hang.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-boats-hang.md diff --git a/.changeset/dirty-boats-hang.md b/.changeset/dirty-boats-hang.md new file mode 100644 index 0000000..2246add --- /dev/null +++ b/.changeset/dirty-boats-hang.md @@ -0,0 +1,5 @@ +--- +formgator: patch +--- + +Added `reportValidity()` function for sveltekit