Skip to content

Commit 01a3c0a

Browse files
authored
[docs] Zod Form example (#1365)
1 parent 6fc23e8 commit 01a3c0a

File tree

7 files changed

+230
-1
lines changed

7 files changed

+230
-1
lines changed

docs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"tailwindcss": "4.0.0-beta.2",
9797
"unified": "^11.0.5",
9898
"webpack-bundle-analyzer": "^4.10.2",
99-
"yargs": "^17.7.2"
99+
"yargs": "^17.7.2",
100+
"zod": "^3.24.1"
100101
}
101102
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
.Form {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 1rem;
5+
width: 100%;
6+
max-width: 16rem;
7+
}
8+
9+
.Field {
10+
display: flex;
11+
flex-direction: column;
12+
align-items: start;
13+
gap: 0.25rem;
14+
}
15+
16+
.Label {
17+
font-size: 0.875rem;
18+
line-height: 1.25rem;
19+
font-weight: 500;
20+
color: var(--color-gray-900);
21+
}
22+
23+
.Input {
24+
box-sizing: border-box;
25+
padding-left: 0.875rem;
26+
margin: 0;
27+
border: 1px solid var(--color-gray-200);
28+
width: 100%;
29+
height: 2.5rem;
30+
border-radius: 0.375rem;
31+
font-family: inherit;
32+
font-size: 1rem;
33+
background-color: transparent;
34+
color: var(--color-gray-900);
35+
36+
&:focus {
37+
outline: 2px solid var(--color-blue);
38+
outline-offset: -1px;
39+
}
40+
}
41+
42+
.Error {
43+
font-size: 0.875rem;
44+
line-height: 1.25rem;
45+
color: var(--color-red-800);
46+
}
47+
48+
.Button {
49+
box-sizing: border-box;
50+
display: flex;
51+
align-items: center;
52+
justify-content: center;
53+
height: 2.5rem;
54+
padding: 0 0.875rem;
55+
margin: 0;
56+
outline: 0;
57+
border: 1px solid var(--color-gray-200);
58+
border-radius: 0.375rem;
59+
background-color: var(--color-gray-50);
60+
font-family: inherit;
61+
font-size: 1rem;
62+
font-weight: 500;
63+
line-height: 1.5rem;
64+
color: var(--color-gray-900);
65+
user-select: none;
66+
67+
@media (hover: hover) {
68+
&:hover {
69+
background-color: var(--color-gray-100);
70+
}
71+
}
72+
73+
&:active {
74+
background-color: var(--color-gray-100);
75+
}
76+
77+
&:disabled {
78+
cursor: not-allowed;
79+
color: var(--color-gray-400);
80+
background-color: var(--color-gray-100);
81+
}
82+
83+
&:focus-visible {
84+
outline: 2px solid var(--color-blue);
85+
outline-offset: -1px;
86+
}
87+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from 'react';
2+
import { z } from 'zod';
3+
import { Field } from '@base-ui-components/react/field';
4+
import { Form } from '@base-ui-components/react/form';
5+
import styles from './index.module.css';
6+
7+
const schema = z.object({
8+
name: z.string().min(1),
9+
age: z.coerce.number().positive(),
10+
});
11+
12+
async function submitForm(event: React.FormEvent<HTMLFormElement>) {
13+
event.preventDefault();
14+
15+
const formData = new FormData(event.currentTarget);
16+
const result = schema.safeParse(Object.fromEntries(formData as any));
17+
18+
if (!result.success) {
19+
return {
20+
errors: result.error.flatten().fieldErrors,
21+
};
22+
}
23+
24+
return {
25+
errors: {},
26+
};
27+
}
28+
29+
export default function Page() {
30+
const [errors, setErrors] = React.useState({});
31+
32+
return (
33+
<Form
34+
className={styles.Form}
35+
errors={errors}
36+
onClearErrors={setErrors}
37+
onSubmit={async (event) => {
38+
const response = await submitForm(event);
39+
setErrors(response.errors);
40+
}}
41+
>
42+
<Field.Root name="name" className={styles.Field}>
43+
<Field.Label className={styles.Label}>Name</Field.Label>
44+
<Field.Control placeholder="Enter name" className={styles.Input} />
45+
<Field.Error className={styles.Error} />
46+
</Field.Root>
47+
<Field.Root name="age" className={styles.Field}>
48+
<Field.Label className={styles.Label}>Age</Field.Label>
49+
<Field.Control placeholder="Enter age" className={styles.Input} />
50+
<Field.Error className={styles.Error} />
51+
</Field.Root>
52+
<button type="submit" className={styles.Button}>
53+
Submit
54+
</button>
55+
</Form>
56+
);
57+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use client';
2+
export { default as CssModules } from './css-modules';
3+
export { default as Tailwind } from './tailwind';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as React from 'react';
2+
import { z } from 'zod';
3+
import { Field } from '@base-ui-components/react/field';
4+
import { Form } from '@base-ui-components/react/form';
5+
6+
const schema = z.object({
7+
name: z.string().min(1),
8+
age: z.coerce.number().positive(),
9+
});
10+
11+
async function submitForm(event: React.FormEvent<HTMLFormElement>) {
12+
event.preventDefault();
13+
14+
const formData = new FormData(event.currentTarget);
15+
const result = schema.safeParse(Object.fromEntries(formData as any));
16+
17+
if (!result.success) {
18+
return {
19+
errors: result.error.flatten().fieldErrors,
20+
};
21+
}
22+
23+
return {
24+
errors: {},
25+
};
26+
}
27+
28+
export default function Page() {
29+
const [errors, setErrors] = React.useState({});
30+
31+
return (
32+
<Form
33+
className="flex w-full max-w-64 flex-col gap-4"
34+
errors={errors}
35+
onClearErrors={setErrors}
36+
onSubmit={async (event) => {
37+
const response = await submitForm(event);
38+
setErrors(response.errors);
39+
}}
40+
>
41+
<Field.Root name="name" className="flex flex-col items-start gap-1">
42+
<Field.Label className="text-sm font-medium text-gray-900">Name</Field.Label>
43+
<Field.Control
44+
placeholder="Enter name"
45+
className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800"
46+
/>
47+
<Field.Error className="text-sm text-red-800" />
48+
</Field.Root>
49+
<Field.Root name="age" className="flex flex-col items-start gap-1">
50+
<Field.Label className="text-sm font-medium text-gray-900">Age</Field.Label>
51+
<Field.Control
52+
placeholder="Enter age"
53+
className="h-10 w-full rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800"
54+
/>
55+
<Field.Error className="text-sm text-red-800" />
56+
</Field.Root>
57+
<button
58+
type="submit"
59+
className="flex h-10 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3.5 text-base font-medium text-gray-900 select-none hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-blue-800 active:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-400"
60+
>
61+
Submit
62+
</button>
63+
</Form>
64+
);
65+
}

docs/src/app/(public)/(content)/react/components/form/page.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ import { Form } from '@base-ui-components/react/form';
2727
```
2828

2929
<Reference component="Form" />
30+
31+
## Examples
32+
33+
### Using with Zod
34+
35+
When parsing the schema using `schema.safeParse()`, the `result.error.flatten().fieldErrors` data can be used to map the errors to each field's `name`.
36+
37+
<Demo path="./demos/zod" />

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)