-
Notifications
You must be signed in to change notification settings - Fork 5
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
Validate by schema (zod
or other)
#17
Comments
This doesn't belong in this library. I use my own zod validator with it in my personal projects. That seems right to me. You're always free to maintain and distribute your own NPM package for this 😃 I understand |
Also this would not work within the design of the library - and that's intentional. It is in the README if you'd like to know why. |
Here is what I'm using: // zod-validate.ts
import type { FieldAtomValidateOn, Validate } from "form-atoms";
import type { Getter } from "jotai";
import type { z } from "zod";
import { ZodError, ZodType } from "zod";
export function zodValidate<Value>(
schema: ((get: Getter) => z.Schema) | z.Schema,
config: ZodValidateConfig = {}
) {
const {
on,
ifDirty,
ifTouched,
formatError = (err) => err.flatten().formErrors,
fatal = false,
} = config;
const ors: ((
state: Parameters<Exclude<Validate<Value>, undefined>>[0]
) => Promise<string[] | undefined>)[] = [];
const chain = Object.assign(
async (
state: Parameters<Exclude<Validate<Value>, undefined>>[0]
): Promise<string[] | undefined> => {
let result: string[] | undefined;
const shouldHandleEvent = !on || on.includes(state.event);
if (shouldHandleEvent) {
const shouldHandleDirty =
ifDirty === undefined || ifDirty === state.dirty;
const shouldHandleTouched =
ifTouched === undefined || ifTouched === state.touched;
if (shouldHandleDirty && shouldHandleTouched) {
const validator =
schema instanceof ZodType ? schema : schema(state.get);
try {
await validator.parseAsync(state.value);
result = [];
} catch (err) {
if (err instanceof ZodError) {
return formatError(err);
}
throw err;
}
}
}
if (ors.length > 0) {
for (const or of ors) {
const errors = await or(state);
if (errors?.length) {
result = errors;
break;
} else if (errors) {
result = errors;
}
if (fatal && result) {
return result;
}
}
}
return result;
},
{
or(config: Omit<ZodValidateConfig, "fatal" | "formatError">) {
const or = zodValidate(schema, { formatError, fatal, ...config });
ors.push(or);
return chain;
},
}
);
return chain;
}
export type ZodValidateConfig = {
on?: FieldAtomValidateOn | FieldAtomValidateOn[];
ifTouched?: boolean;
ifDirty?: boolean;
formatError?: (error: ZodError) => string[];
fatal?: boolean;
}; // zod-validate.test.ts
import { act as domAct, renderHook } from "@testing-library/react";
import { fieldAtom, useFieldAtom } from "form-atoms";
import { z } from "zod";
import { zodValidate } from "./zod-validate";
describe("zodValidate()", () => {
it("should validate without a config", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(z.string().min(3, "3 plz")),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
});
it("should throw multiple errors", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(
z.string().min(3, "3 plz").regex(/foo/, "must match foo"),
{
fatal: false,
}
),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual([
"3 plz",
"must match foo",
]);
});
it("should use custom error formatting", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(
z.string().min(3, "3 plz").regex(/foo/, "must match foo"),
{
formatError: (err) =>
err.errors.map((e) =>
JSON.stringify({ code: e.code, message: e.message })
),
fatal: false,
}
),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual([
JSON.stringify({ code: "too_small", message: "3 plz" }),
JSON.stringify({ code: "invalid_string", message: "must match foo" }),
]);
});
it("should validate 'on' a given event", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(z.string().min(3, "3 plz"), { on: "change" }),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("valid");
expect(field.result.current.state.errors).toEqual([]);
domAct(() => {
field.result.current.actions.setValue("f");
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
});
it("should validate only when dirty", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(z.string().min(3, "3 plz"), {
ifDirty: true,
}),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("valid");
expect(field.result.current.state.errors).toEqual([]);
domAct(() => {
field.result.current.actions.setValue("f");
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
});
it("should validate only when touched", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(z.string().min(3, "3 plz"), {
ifTouched: true,
}),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.setTouched(true);
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
});
it("should validate multiple conditions", async () => {
const nameAtom = fieldAtom({
value: "",
validate: zodValidate(z.string().min(3, "3 plz"), {
on: "user",
}).or({ on: "change", ifDirty: true }),
});
const field = renderHook(() => useFieldAtom(nameAtom));
domAct(() => {
field.result.current.actions.validate();
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
domAct(() => {
field.result.current.actions.setValue("foo bar");
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("valid");
expect(field.result.current.state.errors).toEqual([]);
domAct(() => {
field.result.current.actions.setValue("fo");
});
await domAct(() => Promise.resolve());
expect(field.result.current.state.validateStatus).toBe("invalid");
expect(field.result.current.state.errors).toEqual(["3 plz"]);
});
}); // validate.ts
import type { Getter } from "jotai";
import type { z } from "zod";
import type { ZodValidateConfig } from "./zod-validate";
import { zodValidate } from "./zod-validate";
export const validate = Object.assign(
(
schema: ((get: Getter) => z.Schema) | z.Schema,
config: ValidateConfig = {}
) => {
return zodValidate(schema, {
on: ["submit", "user"],
formatError(error) {
return error.errors.map((err) => JSON.stringify(err));
},
...config,
});
},
{
onBlur: validateOnBlur,
onChange: validateOnChange,
}
);
export function validateOnBlur(
schema: ((get: Getter) => z.Schema) | z.Schema,
config: ValidateConfig = {}
) {
return validate(schema).or({ on: "blur", ifDirty: true, ...config });
}
export function validateOnChange(
schema: ((get: Getter) => z.Schema) | z.Schema,
config: ValidateConfig = {}
) {
return validate(schema)
.or({
on: ["blur"],
ifDirty: true,
})
.or({
on: ["change"],
ifTouched: true,
...config,
});
}
type ValidateConfig = Omit<ZodValidateConfig, "abortEarly" | "formatError">; Usageconst signUpFormAtom = formAtom({
email: fieldAtom({
name: "email",
value: "",
validate: validate.onChange(authSchema.email),
}),
password: fieldAtom({
name: "password",
value: "",
validate: validate.onChange(authSchema.password),
}),
tos: fieldAtom({
name: "tos",
value: true,
validate({ value }) {
if (!value) {
return ["You must accept the terms of service to continue"];
}
return [];
},
}),
}); |
Refactor library to support Jotai v2 breaking changes. Rename form and field hooks to exclude "Atom". Rename types to be consistent across the library. BREAKING CHANGE: Renames form and field hooks to exclude "Atom" and be more terse. Renames most exported types and several type signatures. fix #27 #28 #18 #17
- Refactor library to support Jotai v2 breaking changes. - Rename form and field hooks to exclude "Atom". - Rename types to be consistent across the library. BREAKING CHANGE: Renames form and field hooks to exclude "Atom" and be more terse. Renames most exported types and several type signatures. fix #27 #28 #18 #17
# [2.0.0-next.1](v1.3.0-next.3...v2.0.0-next.1) (2023-02-02) ### Code Refactoring * upgrade to jotai v2 ([#29](#29)) ([e533e40](e533e40)), closes [#27](#27) [#28](#28) [#18](#18) [#17](#17) ### BREAKING CHANGES * Renames form and field hooks to exclude "Atom" and be more terse. Renames most exported types and several type signatures.
# [2.0.0](v1.2.5...v2.0.0) (2023-02-02) ### Bug Fixes * empty arrays not included in submit values ([#31](#31)) ([837140d](837140d)), closes [#26](#26) * fix nested array walk ([#34](#34)) ([448f538](448f538)) * fix package entries ([#24](#24)) ([a18cfc5](a18cfc5)) * fix release ([#22](#22)) ([a4fff3b](a4fff3b)) * fix reset w/ initial value ([#33](#33)) ([8b49243](8b49243)) ### Code Refactoring * upgrade to jotai v2 ([#29](#29)) ([e533e40](e533e40)), closes [#27](#27) [#28](#28) [#18](#18) [#17](#17) ### Features * add zod validator ([#23](#23)) ([06ca2c4](06ca2c4)) * bump next major ([#35](#35)) ([fb3400e](fb3400e)), closes [#20](#20) * update build scripts and tests ([#21](#21)) ([b242e02](b242e02)) ### BREAKING CHANGES * Renames form and field hooks to exclude "Atom" and be more terse. Renames most exported types and several type signatures.
Is your feature request related to a problem? Please describe.
The atoms can be validated by passing
validate
function into the config, but this does not scale well, as we don't want to write code for each field atom separately.Ultimately, the library should accept a schema against which the atoms would automatically validate. This is common practice e.g. in
react-hook-form
.Describe the solution you'd like
Solution would be to provide a integration with one or more popular schema validation libraries:
This reduces the boilerplate code as we can easily validate other fields:
Describe alternatives you've considered
Alternative would be to have the validation not on the individual atoms, but on the
formAtom
which would just accept the schema object and validate each atom respectively.Additional context
The same issue has discussion in the
jotai-form
project:jotaijs/jotai-form#2
The text was updated successfully, but these errors were encountered: