Skip to content

Commit 565c335

Browse files
authored
feat: support zod4 (#169)
1 parent 48b07e2 commit 565c335

34 files changed

+1143
-20
lines changed

.changeset/three-kangaroos-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@autoform/zod": minor
3+
---
4+
5+
add zod v4 support

apps/docs/pages/docs/schema/zod.md

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const formSchema = z.object({
5151
// You can add custom data here
5252
isImportant: true,
5353
},
54-
})
54+
}),
5555
),
5656

5757
favouriteNumber: z.coerce // When using numbers and dates, you must use coerce
@@ -191,7 +191,7 @@ const formSchema = z.object({
191191
z.object({
192192
name: z.string(),
193193
age: z.coerce.number(),
194-
})
194+
}),
195195
)
196196
// Optionally set a custom label - otherwise this will be inferred from the field name
197197
.describe("Guests invited to the party"),
@@ -212,7 +212,7 @@ const formSchema = z.object({
212212
z.object({
213213
name: z.string(),
214214
age: z.coerce.number(),
215-
})
215+
}),
216216
)
217217
.describe("Guests invited to the party")
218218
.default([
@@ -245,7 +245,93 @@ const formSchema = z.object({
245245

246246
You can use the `fieldConfig` function to set additional configuration for how a field should be rendered. This function is independent of the UI library you use so you can provide the FieldTypes that are supported by your UI library.
247247

248-
It's recommended that you create your own fieldConfig function that uses the base fieldConfig function from `@autoform/react` and adds your own customizations:
248+
**Zod v3:**
249+
250+
For Zod v3, it's recommended that you create your own fieldConfig function that uses the base `buildZodFieldConfig` function from `@autoform/react` and adds your own customizations. You apply this configuration using the `.superRefine()` method.
251+
252+
```tsx
253+
import { buildZodFieldConfig } from "@autoform/react";
254+
import { FieldTypes } from "@autoform/mui"; // or your UI library's FieldTypes
255+
256+
const fieldConfig = buildZodFieldConfig<
257+
FieldTypes,
258+
{
259+
isImportant?: boolean;
260+
}
261+
>();
262+
263+
// Example usage with Zod v3
264+
const formSchemaV3 = z.object({
265+
password: z.string().superRefine(
266+
fieldConfig<React.ReactNode, FieldTypes>({
267+
description: "We recommend to use a strong password.",
268+
inputProps: {
269+
type: "password",
270+
},
271+
customData: {
272+
isImportant: true,
273+
},
274+
}),
275+
),
276+
});
277+
```
278+
279+
**Zod v4:**
280+
281+
With Zod v4, the method for applying `fieldConfig` has changed. You now use the `fieldConfig` function directly from `@autoform/zod/v4` and apply it using the `.register()` method provided by Zod v4.
282+
283+
Here's an example demonstrating the new usage:
284+
285+
```tsx
286+
import * as z from "zod/v4";
287+
import { ZodProvider, fieldConfig } from "@autoform/zod/v4"; // Import fieldConfig from v4
288+
import { AutoForm, FieldTypes } from "@autoform/mui"; // or your UI library's FieldTypes
289+
290+
// Define your form schema using zod v4
291+
const formSchemaV4 = z.object({
292+
// ... other fields
293+
294+
password: z
295+
.string({
296+
required_error: "Password is required.",
297+
})
298+
.describe("Your secure password")
299+
.min(8, {
300+
message: "Password must be at least 8 characters.",
301+
})
302+
// Use .register() with the spread operator (...)
303+
.register(
304+
...fieldConfig<React.ReactNode, FieldTypes>({
305+
description: "We recommend to use a strong password.",
306+
inputProps: {
307+
type: "password",
308+
},
309+
customData: {
310+
// You can add custom data here
311+
isImportant: true,
312+
},
313+
}),
314+
),
315+
316+
// ... other fields
317+
});
318+
319+
const schemaProviderV4 = new ZodProvider(formSchemaV4);
320+
321+
function AppV4() {
322+
return (
323+
<AutoForm
324+
schema={schemaProviderV4}
325+
onSubmit={(data) => {
326+
console.log(data);
327+
}}
328+
withSubmit
329+
/>
330+
);
331+
}
332+
```
333+
334+
Notice the use of the spread operator (`...`) before `fieldConfig` when calling `.register()`. This is necessary because `register` expects two arguments, and the `fieldConfig` function from `@autoform/zod/v4` returns a tuple `[key, value]`. Also note that you should import `ZodProvider` and `fieldConfig` from `@autoform/zod/v4` when using Zod v4.
249335

250336
```tsx
251337
import { buildZodFieldConfig } from "@autoform/react";
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from "react";
2+
import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm";
3+
import { ZodProvider, fieldConfig } from "@autoform/zod/v4";
4+
import { z } from "zod/v4";
5+
import { TestWrapper } from "./utils";
6+
7+
describe("AutoForm Advanced Features Tests", () => {
8+
const advancedSchema = z.object({
9+
username: z
10+
.string()
11+
.min(3, "Username must be at least 3 characters")
12+
.register(
13+
...fieldConfig({
14+
description: "Choose a unique username",
15+
order: 1,
16+
inputProps: {
17+
placeholder: "Enter username",
18+
},
19+
}),
20+
),
21+
password: z
22+
.string()
23+
.min(8, "Password must be at least 8 characters")
24+
.register(
25+
...fieldConfig({
26+
description: "Use a strong password",
27+
order: 2,
28+
inputProps: {
29+
type: "password",
30+
},
31+
}),
32+
),
33+
favoriteColor: z.enum(["red", "green", "blue"]).register(
34+
...fieldConfig({
35+
fieldType: "select",
36+
order: 3,
37+
}),
38+
),
39+
bio: z
40+
.string()
41+
.optional()
42+
.register(
43+
...fieldConfig({
44+
order: 4,
45+
}),
46+
),
47+
});
48+
49+
const schemaProvider = new ZodProvider(advancedSchema);
50+
51+
it("renders fields in the correct order", () => {
52+
cy.mount(
53+
<TestWrapper>
54+
<AutoForm
55+
schema={schemaProvider}
56+
onSubmit={cy.stub().as("onSubmit")}
57+
withSubmit
58+
/>
59+
</TestWrapper>,
60+
);
61+
62+
cy.get("input").eq(0).should("have.attr", "name", "username");
63+
cy.get("input").eq(1).should("have.attr", "name", "password");
64+
cy.get("select").should("have.attr", "name", "favoriteColor");
65+
cy.get("input").eq(2).should("have.attr", "name", "bio");
66+
});
67+
68+
it("displays field descriptions", () => {
69+
cy.mount(
70+
<TestWrapper>
71+
<AutoForm
72+
schema={schemaProvider}
73+
onSubmit={cy.stub().as("onSubmit")}
74+
withSubmit
75+
/>
76+
</TestWrapper>,
77+
);
78+
79+
cy.contains("Choose a unique username").should("be.visible");
80+
cy.contains("Use a strong password").should("be.visible");
81+
});
82+
83+
it("applies custom input props", () => {
84+
cy.mount(
85+
<TestWrapper>
86+
<AutoForm
87+
schema={schemaProvider}
88+
onSubmit={cy.stub().as("onSubmit")}
89+
withSubmit
90+
/>
91+
</TestWrapper>,
92+
);
93+
94+
cy.get('input[name="username"]').should(
95+
"have.attr",
96+
"placeholder",
97+
"Enter username",
98+
);
99+
cy.get('input[name="password"]').should("have.attr", "type", "password");
100+
});
101+
102+
it("renders select field correctly", () => {
103+
cy.mount(
104+
<TestWrapper>
105+
<AutoForm
106+
schema={schemaProvider}
107+
onSubmit={cy.stub().as("onSubmit")}
108+
withSubmit
109+
/>
110+
</TestWrapper>,
111+
);
112+
113+
cy.get('[role="combobox"]').should("exist");
114+
cy.get('[role="combobox"]').click();
115+
cy.get("[role='option']").should("have.length", 3);
116+
});
117+
118+
it("renders textarea field correctly", () => {
119+
cy.mount(
120+
<TestWrapper>
121+
<AutoForm
122+
schema={schemaProvider}
123+
onSubmit={cy.stub().as("onSubmit")}
124+
withSubmit
125+
/>
126+
</TestWrapper>,
127+
);
128+
129+
cy.get('input[name="bio"]').should("exist");
130+
});
131+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import "./basic.cy";
2+
import "./arrays.cy";
3+
import "./enums.cy";
4+
import "./subobjects.cy";
5+
import "./form-props.cy";
6+
import "./validation.cy";
7+
import "./custom-fields.cy";
8+
import "./controlled-form.cy";
9+
import "./ui-customization.cy";
10+
import "./advanced-features.cy";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from "react";
2+
import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm";
3+
import { ZodProvider } from "@autoform/zod/v4";
4+
import { z } from "zod/v4";
5+
import { TestWrapper } from "./utils";
6+
7+
describe("AutoForm Arrays Tests", () => {
8+
const arraySchema = z.object({
9+
tags: z.array(z.string()),
10+
friends: z.array(
11+
z.object({
12+
name: z.string(),
13+
age: z.coerce.number(),
14+
}),
15+
),
16+
});
17+
18+
const schemaProvider = new ZodProvider(arraySchema);
19+
20+
it("renders array fields correctly", () => {
21+
cy.mount(
22+
<TestWrapper>
23+
<AutoForm
24+
schema={schemaProvider}
25+
onSubmit={cy.stub().as("onSubmit")}
26+
withSubmit
27+
/>
28+
</TestWrapper>,
29+
);
30+
31+
cy.get(".lucide-plus").should("exist");
32+
cy.get(".lucide-plus").should("exist");
33+
});
34+
35+
it("allows adding and removing array items", () => {
36+
cy.mount(
37+
<TestWrapper>
38+
<AutoForm
39+
schema={schemaProvider}
40+
onSubmit={cy.stub().as("onSubmit")}
41+
withSubmit
42+
/>
43+
</TestWrapper>,
44+
);
45+
46+
// Add tags
47+
cy.get(".lucide-plus").eq(0).click();
48+
cy.get('input[name="tags.0"]').type("tag1");
49+
cy.get(".lucide-plus").eq(0).click();
50+
cy.get('input[name="tags.1"]').type("tag2");
51+
52+
// Add friends
53+
cy.get(".lucide-plus").eq(1).click();
54+
cy.get('input[name="friends.0.name"]').type("Alice");
55+
cy.get('input[name="friends.0.age"]').type("25");
56+
cy.get(".lucide-plus").eq(1).click();
57+
cy.get('input[name="friends.1.name"]').type("Bob");
58+
cy.get('input[name="friends.1.age"]').type("30");
59+
60+
// Remove a tag and a friend
61+
cy.get(".lucide-trash").eq(0).click();
62+
cy.get(".lucide-trash").eq(1).click();
63+
64+
cy.get('button[type="submit"]').click();
65+
66+
cy.get("@onSubmit").should("have.been.calledOnce");
67+
cy.get("@onSubmit").should("have.been.calledWith", {
68+
tags: ["tag2"],
69+
friends: [{ name: "Bob", age: 30 }],
70+
});
71+
});
72+
});

0 commit comments

Comments
 (0)