Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added reportValidity() function for sveltekit #10

Merged
merged 2 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-boats-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
formgator: patch
---

Added `reportValidity()` function for sveltekit
18 changes: 18 additions & 0 deletions demos/sveltekit/src/routes/register/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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 === "[email protected]") {
formfail({ email: "Account already exists" });
}

return { success: true as const, message: "Account created" };
},
),
};
30 changes: 30 additions & 0 deletions demos/sveltekit/src/routes/register/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { enhance } from "$app/forms";
import { reportValidity } from "formgator/sveltekit";

const { form } = $props();
</script>

{#if form?.success}
<p>
{form.message}
</p>
{/if}

<form method="post" use:enhance={reportValidity}>
<p>
<label>
Email:
<input type="email" name="email" required />
</label>
</p>
<p>
<label>
Password:
<input type="password" name="password" required />
</label>
</p>
<p>
<button>Register</button>
</p>
</form>
130 changes: 103 additions & 27 deletions src/sveltekit/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) {
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<string, string>): never {
throw new FormError(issues);
}

/**
* Reports validation issues to the user interface using the `setCustomValidity` API.
*
* Usage: `<form use:enhance={reportValidity} />`
*/
export function reportValidity(): ReturnType<SubmitFunction> {
return ({ update, result, formElement }) => {
update();

if (result.type === "failure" && typeof result.data?.issues === "object") {
const issues = result.data.issues as Record<string, fg.ValidationIssue>;

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).
Expand All @@ -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<string, fg.FormInput>,
ID extends string = string,
>(
inputs: Inputs,
action: Action extends (event: infer Event extends { request: Request; url: URL }) => infer Output
? (data: fg.Infer<Inputs>, event: Event) => Output
: never,
action: (
data: fg.Infer<Inputs>,
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<Inputs>;
accepted: Partial<fg.Infer<Inputs>>;
}>) {
): () =>
| Awaited<Output>
| {
id: ID;
success: false;
issues: fg.InferError<Inputs>;
accepted: Partial<fg.Infer<Inputs>>;
} {
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, {
Expand All @@ -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;
}

Expand Down Expand Up @@ -88,12 +168,8 @@ export function loadgate<Load, Output, Inputs extends Record<string, fg.FormInpu
return ((event: { url: URL }) => {
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;
Expand Down
Loading