Skip to content

Commit

Permalink
feat: Allow users to skip validation or to fallback to server validat…
Browse files Browse the repository at this point in the history
…ion (#50)

* feat: Allow client validation to be skipped or to fallback to server validation

* docs: Document usage of conformValibotMessage
  • Loading branch information
nanto authored Dec 30, 2024
1 parent 1f0ddf7 commit 1318941
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 6 deletions.
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,104 @@ export async function action({ request }) {
}
```

You can skip an validation to use the previous result. On client validation, you can indicate the validation is not defined to fallback to server validation.

```tsx
import type { Intent } from "@conform-to/react";
import { useForm } from '@conform-to/react';
import { parseWithValibot } from 'conform-to-valibot';
import {
check,
forward,
forwardAsync,
object,
partialCheck,
partialCheckAsync,
pipe,
pipeAsync,
string,
} from "valibot";

function createBaseSchema(intent: Intent | null) {
return object({
email: pipe(
string("Email is required"),
// When not validating email, leave the email error as it is.
check(
() =>
intent === null ||
(intent.type === "validate" && intent.payload.name === "email"),
conformValibotMessage.VALIDATION_SKIPPED,
),
),
password: string("Password is required"),
});
}

function createServerSchema(
intent: Intent | null,
options: { isEmailUnique: (email: string) => Promise<boolean> },
) {
return pipeAsync(
createBaseSchema(intent),
forwardAsync(
partialCheckAsync(
[["email"]],
async ({ email }) => options.isEmailUnique(email),
"Email is already used",
),
["email"],
),
);
}

function createClientSchema(intent: Intent | null) {
return pipe(
createBaseSchema(intent),
forward(
// If email is specified, fallback to server validation to check its uniqueness.
partialCheck(
[["email"]],
() => false,
conformValibotMessage.VALIDATION_UNDEFINED,
),
["email"],
),
);
}

export async function action({ request }) {
const formData = await request.formData();
const submission = await parseWithValibot(formData, {
schema: (intent) =>
createServerSchema(intent, {
isEmailUnique: async (email) => {
// Query your database to check if the email is unique
},
}),
});

// Send the submission back to the client if the status is not successful
if (submission.status !== "success") {
return submission.reply();
}

// ...
}

function ExampleForm() {
const [form, { email, password }] = useForm({
onValidate({ formData }) {
return parseWithValibot(formData, {
schema: (intent) => createClientSchema(intent),
});
},
});

// ...
}
```

### getValibotConstraint

A helper that returns an object containing the validation attributes for each field by introspecting the valibot schema.
Expand Down
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { getValibotConstraint } from "./constraint";
export { parseWithValibot } from "./parse";
export { conformValibotMessage, parseWithValibot } from "./parse";
26 changes: 21 additions & 5 deletions parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import {
} from "valibot";
import { enableTypeCoercion } from "./coercion";

export const conformValibotMessage = {
VALIDATION_SKIPPED: "__skipped__",
VALIDATION_UNDEFINED: "__undefined__",
};

type ErrorType = Record<string, string[] | null> | null;

export function parseWithValibot<Schema extends GenericSchema>(
payload: FormData | URLSearchParams,
config: {
Expand Down Expand Up @@ -58,23 +65,32 @@ export function parseWithValibot<

const resolveResult = (
result: SafeParseResult<Schema>,
):
| { value: InferOutput<Schema> }
| { error: Record<string, string[]> } => {
): { value: InferOutput<Schema> } | { error: ErrorType } => {
if (result.success) {
return {
value: result.output,
};
}

return {
error: result.issues.reduce<Record<string, string[]>>((result, e) => {
error: result.issues.reduce<ErrorType>((result, e) => {
if (
result === null ||
e.message === conformValibotMessage.VALIDATION_UNDEFINED
) {
return null;
}

const name = formatPaths(
// @ts-expect-error
e.path?.map((d) => d.key as string | number) ?? [],
);

result[name] = [...(result[name] ?? []), e.message];
result[name] =
result[name] === null ||
e.message === conformValibotMessage.VALIDATION_SKIPPED
? null
: [...(result[name] ?? []), e.message];

return result;
}, {}),
Expand Down
38 changes: 38 additions & 0 deletions tests/parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { check, object, pipe, string } from "valibot";
import { describe, expect, test } from "vitest";
import { conformValibotMessage, parseWithValibot } from "../parse";
import { createFormData } from "./helpers/FormData";

describe("parseWithValibot", () => {
test("should return null for an error field when its error message is skipped", () => {
const schema = object({
key: pipe(
string(),
check(
(input) => input === "valid",
conformValibotMessage.VALIDATION_SKIPPED,
),
),
});
const output = parseWithValibot(createFormData("key", "invalid"), {
schema,
});
expect(output).toMatchObject({ error: { key: null } });
});

test("should return null for the error when any error message is undefined", () => {
const schema = object({
key: pipe(
string(),
check(
(input) => input === "valid",
conformValibotMessage.VALIDATION_UNDEFINED,
),
),
});
const output = parseWithValibot(createFormData("key", "invalid"), {
schema,
});
expect(output).toMatchObject({ error: null });
});
});

0 comments on commit 1318941

Please sign in to comment.