Comments (3)
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 form-atoms/zod
style imports are what some other libraries do, but it's not what I want to do. The community should own it if it wills it and we can put it in the README if it's good.
from form-atoms.
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">;
Usage
const 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 [];
},
}),
});
from form-atoms.
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.
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.
from form-atoms.
Related Issues (20)
- `FormAtomValues` should accept `FormAtom` instead of `FormAtomFields` HOT 2
- Number values cannot be used safely with `<InputField />` nor `useInputField` HOT 23
- Set debug labels for the form atoms HOT 3
- Add `clear` action to support controlling `input[type='file']` HOT 3
- Permit array as root form fields HOT 1
- Use atom id instead of 'unnamed-field' in debugLabels HOT 2
- When input is `required` the `_validateCount` increases while the validate function is not being called HOT 9
- Debug label not set in vite HOT 3
- What's the reason for limiting setting the initial value only once? HOT 10
- Pass setter to the `Validate` callback.
- Outdated lockfile? HOT 3
- Release workflow expression syntax error
- Export `validateAtoms` on `formAtom` similarly to `_validateCallback` on `fieldAtom` HOT 1
- Hydration with `useFieldInitialValue` in Next.js renders twice - first empty, then filled input. HOT 21
- onReset callback HOT 6
- Field & Form atoms should be of `PrimitiveAtom` type HOT 4
- Debug label completion HOT 1
- Examples: Validation Option Valibot along with zod HOT 3
- Bug - the `useField` reads value before initialization HOT 1
- Why the additional re-render? HOT 6
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from form-atoms.