Skip to content

Commit

Permalink
feat: add support for superforms {_errors: string[]} errors (#129)
Browse files Browse the repository at this point in the history
Co-authored-by: Weston Harper <[email protected]>
  • Loading branch information
wesharper and Weston Harper authored Feb 12, 2024
1 parent 6ea3eac commit 04ca5db
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-fireants-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"formsnap": patch
---

Adds support for superforms {\_errors?: string[]} syntax
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.14",
"bits-ui": "^0.9.0",
"bits-ui": "^0.17.0",
"clsx": "^2.0.0",
"concurrently": "^8.2.1",
"contentlayer": "^0.3.4",
Expand Down
42 changes: 25 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/lib/components/form-field.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import { getCleansedErrors } from "..";
import { formFieldProxy } from "sveltekit-superforms/client";
import { writable } from "svelte/store";
import { createFormField, createIds } from "@/lib/internal/index.js";
Expand All @@ -18,16 +20,18 @@
export let name: Path;
const attrStore: FieldAttrStore = writable({});
$: ({ errors, value, constraints } = formFieldProxy<T, Path>(config.form, name));
$: ({ errors: rawErrors, value, constraints } = formFieldProxy<T, Path>(config.form, name));
$: errors = getCleansedErrors($rawErrors);
const ids = writable(createIds());
$: ({ getFieldAttrs, actions, hasValidation, hasDescription, handlers, setValue } =
createFormField<T, Path>(name, attrStore, value, errors, ids));
createFormField<T, Path>(name, attrStore, value, rawErrors, ids));
$: inputAttrs = getFieldAttrs({
val: $value,
errors: $errors,
errors: errors,
constraints: $constraints,
hasValidation: $hasValidation,
hasDescription: $hasDescription
Expand Down Expand Up @@ -56,7 +60,7 @@

<slot
{stores}
errors={$errors}
{errors}
value={$value}
constraints={$constraints}
{handlers}
Expand Down
11 changes: 7 additions & 4 deletions src/lib/components/form-validation.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
<script lang="ts">
import { getFormField } from "@/lib/index.js";
import { getFormField, getCleansedErrors } from "@/lib/index.js";
import type { ValidationProps } from "../types.js";
type $$Props = ValidationProps;
export let tag = "p";
const { actions, errors, ids } = getFormField();
$: internalErrors = getCleansedErrors($errors);
$: attrs = {
"data-fs-validation": "",
"data-fs-error": $errors ? "" : undefined,
"data-fs-error": internalErrors ? "" : undefined,
id: $ids.validation
};
</script>

<svelte:element this={tag} use:actions.validation {...attrs} {...$$restProps}>
{#if $errors}
{$errors}
{#if internalErrors}
{internalErrors}
{/if}
</svelte:element>
5 changes: 5 additions & 0 deletions src/lib/helpers/get-cleansed-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { SuperformsValidationError } from "../internal";

export function getCleansedErrors(errors: SuperformsValidationError): string[] | undefined {
return Array.isArray(errors) ? errors : errors?._errors ? errors._errors : undefined;
}
1 change: 1 addition & 0 deletions src/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./get-form-field.js";
export * from "./get-form.js";
export * from "./get-form-schema.js";
export * from "./get-form-control.js";
export * from "./get-cleansed-errors.js";
5 changes: 4 additions & 1 deletion src/lib/internal/form-field/create-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createFieldActions, createFieldHandlers } from "@/lib/internal/index.js
import type { ErrorsStore, GetFieldAttrsProps } from "@/lib/internal/index.js";
import type { CreateFormFieldReturn, FieldAttrStore, FieldContext, FieldIds } from "./types.js";
import { setContext } from "svelte";
import { getCleansedErrors } from "@/lib/index.js";

export const FORM_FIELD_CONTEXT = "FormField";

Expand Down Expand Up @@ -52,7 +53,9 @@ export function createFormField<
setContext(FORM_FIELD_CONTEXT, context);

function getFieldAttrs<T>(props: GetFieldAttrsProps<T>) {
const { val, errors, constraints, hasValidation, hasDescription } = props;
const { val, errors: rawErrors, constraints, hasValidation, hasDescription } = props;
const errors = getCleansedErrors(rawErrors);

const $ids = get(ids);
const describedBy = errors
? `${hasValidation ? $ids.validation : ""} ${hasDescription ? $ids.description : ""}`
Expand Down
4 changes: 3 additions & 1 deletion src/lib/internal/form-field/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type CreateFormFieldReturn = {
context: FieldContext;
};

export type SuperformsValidationError = { _errors?: string[] } | string[] | undefined;

export type FieldContext = {
/**
* The name of the field in the form schema.
Expand All @@ -32,7 +34,7 @@ export type FieldContext = {
* if they exist, or undefined if they don't. Useful for displaying
* errors in a custom validation message component.
*/
errors: Writable<string[] | undefined>;
errors: Writable<SuperformsValidationError>;

/**
* A writable store containing the current value of the field.
Expand Down
48 changes: 47 additions & 1 deletion src/routes/test/a/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@
}),
bio: z.string().max(250, "Bio must be at most 250 characters").optional(),
website: z.string().url("Invalid URL").optional(),
usage: z.boolean().default(true)
usage: z.boolean().default(true),
multiSelect: z.array(z.string()).nonempty()
});
const multiSelectItems = [
{ label: "First", value: "first" },
{ label: "Second", value: "second" },
{ label: "Third", value: "third" },
{ label: "Fourth", value: "fourth" }
];
</script>

<script lang="ts">
import { Form } from "@/lib/index.js";
import type { PageData } from "./$types.js";
import { Button } from "@/components/ui/button/index.js";
import { Select } from "bits-ui";
export let data: PageData;
</script>
Expand Down Expand Up @@ -103,6 +112,43 @@
<Form.Description data-testid="usage-description">usage description</Form.Description>
</div>
</Form.Field>
<Form.Field {config} name="multiSelect" let:setValue let:value>
<Form.Label>Multi-select</Form.Label>
<Select.Root
items={multiSelectItems}
let:ids
multiple={true}
onSelectedChange={(selected) => {
setValue(selected?.map((opt) => opt.value));
}}
>
<Select.Input />
<Form.Control let:attrs id={ids.trigger}>
<Select.Trigger
{...attrs}
class="flex h-9 w-full items-center justify-between rounded-md border-0 bg-white px-3 py-1.5 text-gray-900 shadow-sm outline-none ring-1 ring-inset ring-gray-300 transition-opacity placeholder:text-gray-400 hover:opacity-90 focus:ring-2 focus:ring-inset focus:ring-violet-600 sm:text-sm sm:leading-6"
>
<Select.Value placeholder="Select one or more" class="truncate" />
&downarrow;
</Select.Trigger>
<Select.Content
class="z-10 flex max-h-72 select-none flex-col overflow-y-auto rounded-md bg-white shadow outline-none focus:ring-2 focus:ring-violet-700"
>
{#each multiSelectItems as option}
<Select.Item
value={option.value}
label={option.label}
class="flex cursor-pointer select-none items-center justify-between px-4 py-1 text-gray-800 outline-none data-[highlighted]:!bg-violet-900 data-[selected]:bg-violet-100 data-[highlighted]:!text-violet-100 data-[selected]:text-violet-900"
>
{option.label}
<Select.ItemIndicator>&check;</Select.ItemIndicator>
</Select.Item>
{/each}
</Select.Content>
</Form.Control>
</Select.Root>
<Form.Validation class="text-destructive" />
</Form.Field>
<Button type="submit" data-testid="submit">Submit</Button>
</Form.Root>
</div>

0 comments on commit 04ca5db

Please sign in to comment.