- react hook form and it's zod integration is a better way IMHO (https://github.com/jsun969/react-hook-form-antd)
- Might only work in Antd v4
- Don't add
rules
on the schema relatedForm.Item
components. - Might not do everything you need (e.g. async?)
- May have bugs. But what doesn't 🤣
The hook found in zodAntdFormAdapter.tsx
might be useful when trying to
run zod schema validation when the form is changed by the user.
It takes the form instance, the schema, and onFinish
and onFinishFailed
callback functions. The expected onFinish
callback differs a bit from the
callback you would normally pass to the Form
component as it tries to infer
the type of the values from the passed in schema. onFinishFailed
has the
same signature as the regular form prop though.
The hook basically adds a watch callback to the form store that runs the schema validation and sets the schema errors on the matching form fields whenever the form changes.
It returns a props object which is meant to be spread into the form props. The
props provide wrapped onFinish
and onFinishFailed
callbacks that take care of
calling your callbacks, taking the schema validation into account.
For convenience, the props also contain the form
prop already set.
import { countBy } from "lodash";
import { z } from "zod";
import { Button, Card, Form, Input } from "antd";
import { useZodAntdFormAdapter } from "./zodAntdFormAdapter";
const initialValues = {
name: "foo",
items: [{ id: "foo" }],
};
const schema = z.object({
name: z.string().min(3),
items: z.array(
z.object({
id: z.string().min(3),
})
).superRefine((items, ctx) => {
const idCounts = countBy(items, "id");
items.forEach(({ id }, index) => {
if (idCounts[id] > 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "The item id has to be unique",
path: [index, "id"],
});
}
});
}),
});
function MyForm() {
const [form] = Form.useForm();
const formAdapter = useZodAntdFormAdapter({
form,
schema,
onFinish(values) {
console.log("my onFinish", { values });
},
onFinishFailed(error) {
console.log("my onFinishFailed", { error });
},
});
return (
<Form
{...formAdapter}
initialValues={initialValues}
>
<Form.Item name={["name"]} label="Name" required>
<Input />
</Form.Item>
<Form.List name={["items"]}>
{(fields, { add, remove }) => (
<Form.Item label="Items">
<Button onClick={() => add({ "id": "bar" })}>Add one</Button>
{fields.map((field) => (
<Card key={field.key}>
<Form.Item name={[field.name, "id"]} label="Item id" required>
<Input />
</Form.Item>
<Button onClick={() => remove(field.name)}>Remove</Button>
</Card>
))}
</Form.Item>
)}
</Form.List>
<Button onClick={form.submit}>Submit</Button>
</Form>
);
}
@grodrigues101 it would, of course, be possible but would require adjusting antd's form and rc-field-form code. One could also go on and have a rules generating function on all fields, but I found the rules themselves conflicting with hooking validation on field changes and the finish callbacks (which I needed to do to run custom validation logic on form lists, and integrating better with dnd reordering for example) and cause the validation to run even more often with the approach in the gist (hooking into field changes).
Given the way rc-field-form currently handles the form state, it would seem to me that those adjustments would be quite involved. And if we are already busy converting everything, one might as well do it in such a way that you can integrate different schema libraries.
I am still not finished with the part of the application I am using this snippet in though, so I will try to update it once that it is sufficiently finished.
I guess we could also add a helper function to just infer the
required
prop if that would help but that would require to pass around the schema in some way to nested components and add the helper to all relevant form item components. Or we could try to sneak in the schema by adding it to theFormInstance
and then have a wrapper component forForm.Item
that retrieves the schema from the field context. But is that something we would want?