@primereact/mcp 11.0.0 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/llms/headless/components/sidebar/api.json +1029 -54
- package/data/llms/headless/guides/misc/internationalization.md +282 -0
- package/data/llms/headless/guides/misc/locale.md +280 -0
- package/data/llms/llms-full.txt +9822 -1377
- package/data/llms/llms.txt +15 -0
- package/data/llms/primitive/components/sidebar/api.json +127 -33
- package/data/llms/primitive/guides/migration/updating-to-v11.md +2219 -0
- package/data/llms/primitive/guides/misc/internationalization.md +287 -0
- package/data/llms/primitive/guides/misc/locale.md +280 -0
- package/data/llms/styled/add-ons/designer/ci.md +273 -0
- package/data/llms/styled/add-ons/designer/guide.md +99 -0
- package/data/llms/styled/add-ons/designer/overview.md +194 -0
- package/data/llms/styled/add-ons/uikit/guide/v3.md +181 -0
- package/data/llms/styled/add-ons/uikit/guide/v4.md +163 -0
- package/data/llms/styled/add-ons/uikit/overview.md +204 -0
- package/data/llms/styled/components/datatable.md +8 -5
- package/data/llms/styled/components/menu.md +39 -41
- package/data/llms/styled/guides/configuration.md +198 -0
- package/data/llms/styled/guides/form/formik.md +448 -0
- package/data/llms/styled/guides/form/react-hook-form.md +503 -0
- package/data/llms/styled/guides/form/tanstack.md +502 -0
- package/data/llms/styled/guides/migration/updating-to-v11.md +2219 -0
- package/data/llms/styled/guides/misc/internationalization.md +379 -0
- package/data/llms/styled/guides/misc/locale.md +280 -0
- package/data/llms/tailwind/components/menu.md +5 -5
- package/data/llms/tailwind/components/tooltip.md +4 -13
- package/data/llms/tailwind/guides/misc/internationalization.md +287 -0
- package/data/llms/tailwind/guides/misc/locale.md +280 -0
- package/data/manifest.json +339 -24
- package/data/mcp-data.json +263 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# TanStack Form
|
|
2
|
+
|
|
3
|
+
Build validated forms with PrimeReact and TanStack Form.
|
|
4
|
+
|
|
5
|
+
<DocMessage>
|
|
6
|
+
Open the <a href="https://stackblitz.com/edit/vitejs-vite-bluiv8e7?file=src%2Fcomponents%2Ftanstack-form.tsx" target="_blank" rel="noopener noreferrer">complete example on StackBlitz</a> to run and edit the form built in this guide.
|
|
7
|
+
</DocMessage>
|
|
8
|
+
|
|
9
|
+
## Introduction
|
|
10
|
+
|
|
11
|
+
[TanStack Form](https://tanstack.com/form) is a headless, fully type-safe form library. Fields are declared with the `form.Field` component, and each one owns its value and handlers. PrimeReact inputs are controlled, so they read from `field.state.value` and report through `field.handleChange`.
|
|
12
|
+
|
|
13
|
+
TanStack Form implements the [Standard Schema](https://github.com/standard-schema/standard-schema) spec, so a [Zod](https://zod.dev/) schema plugs straight into `validators` with no adapter.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @tanstack/react-form zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Anatomy
|
|
20
|
+
|
|
21
|
+
A field is a `form.Field` whose render callback receives a `field` object. Read the value from `field.state.value`, push changes with `field.handleChange`, and mark it visited with `field.handleBlur`:
|
|
22
|
+
|
|
23
|
+
```tsx title="Anatomy" showLineNumbers
|
|
24
|
+
<form.Field name="projectName">
|
|
25
|
+
{(field) => (
|
|
26
|
+
<InputText
|
|
27
|
+
id={field.name}
|
|
28
|
+
value={field.state.value} // current value
|
|
29
|
+
onChange={(e) => field.handleChange(e.target.value)} // push the change
|
|
30
|
+
onBlur={field.handleBlur} // mark as touched
|
|
31
|
+
invalid={field.state.meta.errors.length > 0} // error styling
|
|
32
|
+
/>
|
|
33
|
+
)}
|
|
34
|
+
</form.Field>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **`field.state.value`**: the current value of this field.
|
|
38
|
+
- **`field.handleChange` / `field.handleBlur`**: update the value and the touched state.
|
|
39
|
+
- **`field.state.meta`**: the field's status (`errors`, `isTouched`, `isDirty`, `isValidating`).
|
|
40
|
+
|
|
41
|
+
Native inputs report a DOM event, so pass `e.target.value`. Composite components report their own value, covered in [Fields](#fields).
|
|
42
|
+
|
|
43
|
+
## Create the form
|
|
44
|
+
|
|
45
|
+
### Schema
|
|
46
|
+
|
|
47
|
+
Describe the shape and rules of the form with Zod. The message passed to each rule is what surfaces as the field error:
|
|
48
|
+
|
|
49
|
+
```tsx title="schema.ts" showLineNumbers
|
|
50
|
+
import { z } from 'zod';
|
|
51
|
+
|
|
52
|
+
export const schema = z.object({
|
|
53
|
+
projectName: z.string().min(1, 'Project name is required.'),
|
|
54
|
+
framework: z.string().min(1, 'Please select a framework.'),
|
|
55
|
+
rootDir: z.string().min(1, 'Root directory is required.'),
|
|
56
|
+
environments: z.array(z.string()).min(1, 'Select at least one environment.')
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type FormValues = z.infer<typeof schema>;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Connect to TanStack Form
|
|
63
|
+
|
|
64
|
+
Create the form with `useForm`. Provide `defaultValues` for every field, pass the schema to `validators`, and handle the typed `value` in `onSubmit`:
|
|
65
|
+
|
|
66
|
+
```tsx title="deploy-form.tsx" showLineNumbers {5,6}
|
|
67
|
+
import { useForm } from '@tanstack/react-form';
|
|
68
|
+
import { schema } from './schema';
|
|
69
|
+
|
|
70
|
+
const form = useForm({
|
|
71
|
+
defaultValues: { projectName: '', framework: '', rootDir: './', environments: [] as string[] },
|
|
72
|
+
validators: { onChange: schema },
|
|
73
|
+
onSubmit: ({ value }) => {
|
|
74
|
+
console.log(value);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
<DocMessage>
|
|
80
|
+
`validators` accepts any Standard Schema validator, so you can swap Zod for Valibot or ArkType without changing anything else. Attach `onBlur` or `onSubmit` validators instead of `onChange` to control timing.
|
|
81
|
+
</DocMessage>
|
|
82
|
+
|
|
83
|
+
### Submit
|
|
84
|
+
|
|
85
|
+
TanStack Form does not hijack the native submit, so prevent the default and call `form.handleSubmit` yourself. It validates first and only runs your `onSubmit` when every field passes:
|
|
86
|
+
|
|
87
|
+
```tsx title="Submit" showLineNumbers
|
|
88
|
+
return (
|
|
89
|
+
<form
|
|
90
|
+
onSubmit={(e) => {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
form.handleSubmit();
|
|
94
|
+
}}
|
|
95
|
+
className="flex flex-col gap-6"
|
|
96
|
+
>
|
|
97
|
+
{/* fields go here */}
|
|
98
|
+
<Button type="submit">Deploy</Button>
|
|
99
|
+
</form>
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Reset
|
|
104
|
+
|
|
105
|
+
Call `form.reset()` to clear every field back to `defaultValues`, or pass an object to seed the form with new values instead, for example after a successful submit:
|
|
106
|
+
|
|
107
|
+
```tsx title="Reset"
|
|
108
|
+
// restore the defaults
|
|
109
|
+
<Button type="button" severity="secondary" variant="outlined" onClick={() => form.reset()}>
|
|
110
|
+
Reset
|
|
111
|
+
</Button>;
|
|
112
|
+
|
|
113
|
+
// or set new values
|
|
114
|
+
form.reset({ projectName: 'my-app', framework: 'nextjs', rootDir: './', environments: ['production'] });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Result
|
|
118
|
+
|
|
119
|
+
Everything above comes together as the complete form. `form.Subscribe` reads the live form state so the submit button can disable itself while the form is invalid or submitting. Each field is broken down in [Fields](#fields):
|
|
120
|
+
|
|
121
|
+
<div className="[&_pre]:max-h-[32rem] [&_pre]:overflow-y-auto">
|
|
122
|
+
|
|
123
|
+
```tsx title="deploy-form.tsx" showLineNumbers
|
|
124
|
+
'use client';
|
|
125
|
+
import { useForm } from '@tanstack/react-form';
|
|
126
|
+
import { z } from 'zod';
|
|
127
|
+
import { ChevronDown, Check } from '@primeicons/react';
|
|
128
|
+
import { InputText } from '@primereact/ui/inputtext';
|
|
129
|
+
import { Select } from '@primereact/ui/select';
|
|
130
|
+
import { Checkbox } from '@primereact/ui/checkbox';
|
|
131
|
+
import { CheckboxGroup } from '@primereact/ui/checkboxgroup';
|
|
132
|
+
import { Label } from '@primereact/ui/label';
|
|
133
|
+
import { Button } from '@primereact/ui/button';
|
|
134
|
+
import { Message } from '@primereact/ui/message';
|
|
135
|
+
|
|
136
|
+
const FRAMEWORKS = [
|
|
137
|
+
{ label: 'Next.js', value: 'nextjs' },
|
|
138
|
+
{ label: 'Vite', value: 'vite' },
|
|
139
|
+
{ label: 'Astro', value: 'astro' }
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const ENVIRONMENTS = [
|
|
143
|
+
{ label: 'Production', value: 'production' },
|
|
144
|
+
{ label: 'Preview', value: 'preview' },
|
|
145
|
+
{ label: 'Development', value: 'development' }
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const schema = z.object({
|
|
149
|
+
projectName: z.string().min(1, 'Project name is required.'),
|
|
150
|
+
framework: z.string().min(1, 'Please select a framework.'),
|
|
151
|
+
rootDir: z.string().min(1, 'Root directory is required.'),
|
|
152
|
+
environments: z.array(z.string()).min(1, 'Select at least one environment.')
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export default function DeployForm() {
|
|
156
|
+
const form = useForm({
|
|
157
|
+
defaultValues: { projectName: '', framework: '', rootDir: './', environments: [] as string[] },
|
|
158
|
+
validators: { onChange: schema },
|
|
159
|
+
onSubmit: ({ value }) => {
|
|
160
|
+
console.log(value);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<form
|
|
166
|
+
onSubmit={(e) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
form.handleSubmit();
|
|
170
|
+
}}
|
|
171
|
+
className="flex flex-col gap-6"
|
|
172
|
+
>
|
|
173
|
+
<form.Field name="projectName">
|
|
174
|
+
{(field) => (
|
|
175
|
+
<div className="flex flex-col gap-2">
|
|
176
|
+
<Label htmlFor={field.name}>Project name</Label>
|
|
177
|
+
<InputText id={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} invalid={field.state.meta.errors.length > 0} fluid />
|
|
178
|
+
{field.state.meta.errors.length > 0 && (
|
|
179
|
+
<Message.Root severity="error" variant="simple" size="small">
|
|
180
|
+
<Message.Content>
|
|
181
|
+
<Message.Text>{field.state.meta.errors[0]?.message}</Message.Text>
|
|
182
|
+
</Message.Content>
|
|
183
|
+
</Message.Root>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</form.Field>
|
|
188
|
+
|
|
189
|
+
<form.Field name="framework">
|
|
190
|
+
{(field) => (
|
|
191
|
+
<div className="flex flex-col gap-2">
|
|
192
|
+
<Label htmlFor={field.name}>Framework preset</Label>
|
|
193
|
+
<Select.Root
|
|
194
|
+
value={field.state.value}
|
|
195
|
+
onValueChange={(e) => field.handleChange(e.value as string)}
|
|
196
|
+
onBlur={field.handleBlur}
|
|
197
|
+
options={FRAMEWORKS}
|
|
198
|
+
optionLabel="label"
|
|
199
|
+
optionValue="value"
|
|
200
|
+
invalid={field.state.meta.errors.length > 0}
|
|
201
|
+
>
|
|
202
|
+
<Select.Trigger type="button">
|
|
203
|
+
<Select.Value placeholder="Select a framework" />
|
|
204
|
+
<Select.Indicator>
|
|
205
|
+
<ChevronDown />
|
|
206
|
+
</Select.Indicator>
|
|
207
|
+
</Select.Trigger>
|
|
208
|
+
<Select.Portal>
|
|
209
|
+
<Select.Positioner>
|
|
210
|
+
<Select.Popup>
|
|
211
|
+
<Select.List />
|
|
212
|
+
</Select.Popup>
|
|
213
|
+
</Select.Positioner>
|
|
214
|
+
</Select.Portal>
|
|
215
|
+
</Select.Root>
|
|
216
|
+
{field.state.meta.errors.length > 0 && (
|
|
217
|
+
<Message.Root severity="error" variant="simple" size="small">
|
|
218
|
+
<Message.Content>
|
|
219
|
+
<Message.Text>{field.state.meta.errors[0]?.message}</Message.Text>
|
|
220
|
+
</Message.Content>
|
|
221
|
+
</Message.Root>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</form.Field>
|
|
226
|
+
|
|
227
|
+
<form.Field name="rootDir">
|
|
228
|
+
{(field) => (
|
|
229
|
+
<div className="flex flex-col gap-2">
|
|
230
|
+
<Label htmlFor={field.name}>Root directory</Label>
|
|
231
|
+
<InputText id={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} invalid={field.state.meta.errors.length > 0} fluid />
|
|
232
|
+
{field.state.meta.errors.length > 0 && (
|
|
233
|
+
<Message.Root severity="error" variant="simple" size="small">
|
|
234
|
+
<Message.Content>
|
|
235
|
+
<Message.Text>{field.state.meta.errors[0]?.message}</Message.Text>
|
|
236
|
+
</Message.Content>
|
|
237
|
+
</Message.Root>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</form.Field>
|
|
242
|
+
|
|
243
|
+
<form.Field name="environments">
|
|
244
|
+
{(field) => (
|
|
245
|
+
<div className="flex flex-col gap-2">
|
|
246
|
+
<Label>Environments</Label>
|
|
247
|
+
<CheckboxGroup value={field.state.value} onValueChange={(e) => field.handleChange(e.value as string[])}>
|
|
248
|
+
{ENVIRONMENTS.map((env) => (
|
|
249
|
+
<div key={env.value} className="flex items-center gap-2">
|
|
250
|
+
<Checkbox.Root inputId={env.value} value={env.value}>
|
|
251
|
+
<Checkbox.Box>
|
|
252
|
+
<Checkbox.Indicator match="checked">
|
|
253
|
+
<Check />
|
|
254
|
+
</Checkbox.Indicator>
|
|
255
|
+
</Checkbox.Box>
|
|
256
|
+
</Checkbox.Root>
|
|
257
|
+
<Label htmlFor={env.value}>{env.label}</Label>
|
|
258
|
+
</div>
|
|
259
|
+
))}
|
|
260
|
+
</CheckboxGroup>
|
|
261
|
+
{field.state.meta.errors.length > 0 && (
|
|
262
|
+
<Message.Root severity="error" variant="simple" size="small">
|
|
263
|
+
<Message.Content>
|
|
264
|
+
<Message.Text>{field.state.meta.errors[0]?.message}</Message.Text>
|
|
265
|
+
</Message.Content>
|
|
266
|
+
</Message.Root>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</form.Field>
|
|
271
|
+
|
|
272
|
+
<div className="flex justify-end gap-3">
|
|
273
|
+
<Button type="button" severity="secondary" variant="outlined" onClick={() => form.reset()}>
|
|
274
|
+
Reset
|
|
275
|
+
</Button>
|
|
276
|
+
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
|
|
277
|
+
{([canSubmit, isSubmitting]) => (
|
|
278
|
+
<Button type="submit" disabled={!canSubmit}>
|
|
279
|
+
{isSubmitting ? 'Deploying…' : 'Deploy'}
|
|
280
|
+
</Button>
|
|
281
|
+
)}
|
|
282
|
+
</form.Subscribe>
|
|
283
|
+
</div>
|
|
284
|
+
</form>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
## Fields
|
|
292
|
+
|
|
293
|
+
Every field is a `form.Field`. How you forward the change depends on the component:
|
|
294
|
+
|
|
295
|
+
- **Native inputs**: `InputText` and `Textarea` render a real `<input>` / `<textarea>` and report a DOM event, so pass `e.target.value` to `field.handleChange`.
|
|
296
|
+
- **Composite inputs**: `Select`, `Checkbox`, `RadioButtonGroup` and `CheckboxGroup` report through `onValueChange` / `onCheckedChange` with an event object, so forward `event.value` / `event.checked`.
|
|
297
|
+
|
|
298
|
+
Bind `invalid={field.state.meta.errors.length > 0}` on any component to get error styling.
|
|
299
|
+
|
|
300
|
+
### InputText
|
|
301
|
+
|
|
302
|
+
```tsx title="InputText"
|
|
303
|
+
<form.Field name="projectName">
|
|
304
|
+
{(field) => (
|
|
305
|
+
<div className="flex flex-col gap-2">
|
|
306
|
+
<Label htmlFor={field.name}>Project name</Label>
|
|
307
|
+
<InputText id={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} invalid={field.state.meta.errors.length > 0} fluid />
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</form.Field>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Select
|
|
314
|
+
|
|
315
|
+
```tsx title="Select"
|
|
316
|
+
<form.Field name="framework">
|
|
317
|
+
{(field) => (
|
|
318
|
+
<Select.Root value={field.state.value} onValueChange={(e) => field.handleChange(e.value as string)} onBlur={field.handleBlur} options={FRAMEWORKS} optionLabel="label" optionValue="value" invalid={field.state.meta.errors.length > 0}>
|
|
319
|
+
<Select.Trigger type="button">
|
|
320
|
+
<Select.Value placeholder="Select a framework" />
|
|
321
|
+
<Select.Indicator>
|
|
322
|
+
<ChevronDown />
|
|
323
|
+
</Select.Indicator>
|
|
324
|
+
</Select.Trigger>
|
|
325
|
+
<Select.Portal>
|
|
326
|
+
<Select.Positioner>
|
|
327
|
+
<Select.Popup>
|
|
328
|
+
<Select.List />
|
|
329
|
+
</Select.Popup>
|
|
330
|
+
</Select.Positioner>
|
|
331
|
+
</Select.Portal>
|
|
332
|
+
</Select.Root>
|
|
333
|
+
)}
|
|
334
|
+
</form.Field>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Checkbox
|
|
338
|
+
|
|
339
|
+
A single checkbox is a boolean, so bind `checked` and forward `event.checked`:
|
|
340
|
+
|
|
341
|
+
```tsx title="Checkbox"
|
|
342
|
+
<form.Field name="agree">
|
|
343
|
+
{(field) => (
|
|
344
|
+
<Checkbox.Root checked={field.state.value} onCheckedChange={(e) => field.handleChange(e.checked)}>
|
|
345
|
+
<Checkbox.Box>
|
|
346
|
+
<Checkbox.Indicator match="checked">
|
|
347
|
+
<Check />
|
|
348
|
+
</Checkbox.Indicator>
|
|
349
|
+
</Checkbox.Box>
|
|
350
|
+
</Checkbox.Root>
|
|
351
|
+
)}
|
|
352
|
+
</form.Field>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Groups
|
|
356
|
+
|
|
357
|
+
`RadioButtonGroup`, `CheckboxGroup` and `ToggleButtonGroup` own a single value for the whole group, so wrap the group (not each item) in one `form.Field`:
|
|
358
|
+
|
|
359
|
+
```tsx title="CheckboxGroup"
|
|
360
|
+
<form.Field name="environments">
|
|
361
|
+
{(field) => (
|
|
362
|
+
<CheckboxGroup value={field.state.value} onValueChange={(e) => field.handleChange(e.value as string[])}>
|
|
363
|
+
{ENVIRONMENTS.map((env) => (
|
|
364
|
+
<div key={env.value} className="flex items-center gap-2">
|
|
365
|
+
<Checkbox.Root inputId={env.value} value={env.value}>
|
|
366
|
+
<Checkbox.Box>
|
|
367
|
+
<Checkbox.Indicator match="checked">
|
|
368
|
+
<Check />
|
|
369
|
+
</Checkbox.Indicator>
|
|
370
|
+
</Checkbox.Box>
|
|
371
|
+
</Checkbox.Root>
|
|
372
|
+
<Label htmlFor={env.value}>{env.label}</Label>
|
|
373
|
+
</div>
|
|
374
|
+
))}
|
|
375
|
+
</CheckboxGroup>
|
|
376
|
+
)}
|
|
377
|
+
</form.Field>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Value types
|
|
381
|
+
|
|
382
|
+
Most inputs hold a string, but some do not. Match the schema and the `defaultValues` to the value type:
|
|
383
|
+
|
|
384
|
+
- `InputNumber`, `Slider`, `Rating` → `number` (`z.number()`)
|
|
385
|
+
- `DatePicker` → `Date` (`z.date()`)
|
|
386
|
+
- `Checkbox`, `ToggleSwitch`, `ToggleButton` → `boolean` (`z.boolean()`)
|
|
387
|
+
- `CheckboxGroup`, multiple `Select`, `InputTags` → `array` (`z.array(...)`)
|
|
388
|
+
|
|
389
|
+
<DocMessage>
|
|
390
|
+
Number and date fields start as `null`, not `''`. An empty string breaks their value type.
|
|
391
|
+
</DocMessage>
|
|
392
|
+
|
|
393
|
+
## Validation
|
|
394
|
+
|
|
395
|
+
### When it runs
|
|
396
|
+
|
|
397
|
+
Attach the schema to the timing you want under `validators`. You can set them on the whole form or on an individual `form.Field`:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// whole form
|
|
401
|
+
useForm({ validators: { onChange: schema } });
|
|
402
|
+
|
|
403
|
+
// single field
|
|
404
|
+
<form.Field name="projectName" validators={{ onBlur: z.string().min(1, 'Required.') }}>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
| Validator | Runs |
|
|
408
|
+
| --------------- | -------------------------------- |
|
|
409
|
+
| `onChange` | on every keystroke / change |
|
|
410
|
+
| `onBlur` | when a field loses focus |
|
|
411
|
+
| `onSubmit` | on submit only |
|
|
412
|
+
| `onChangeAsync` | debounced async check (e.g. API) |
|
|
413
|
+
|
|
414
|
+
Field-level validators run alongside the form-level ones, so you can layer a cheap sync rule with an async uniqueness check.
|
|
415
|
+
|
|
416
|
+
### Showing errors
|
|
417
|
+
|
|
418
|
+
`field.state.meta.errors` is an array of the schema issues for that field. Render the first message with the `Message` component in the `simple` variant so it sits inline under the field:
|
|
419
|
+
|
|
420
|
+
```tsx title="Error message"
|
|
421
|
+
<form.Field name="projectName">
|
|
422
|
+
{(field) => (
|
|
423
|
+
<div className="flex flex-col gap-2">
|
|
424
|
+
<Label htmlFor={field.name}>Project name</Label>
|
|
425
|
+
<InputText id={field.name} value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} onBlur={field.handleBlur} invalid={field.state.meta.errors.length > 0} fluid />
|
|
426
|
+
{field.state.meta.errors.length > 0 && (
|
|
427
|
+
<Message.Root severity="error" variant="simple" size="small">
|
|
428
|
+
<Message.Content>
|
|
429
|
+
<Message.Text>{field.state.meta.errors[0]?.message}</Message.Text>
|
|
430
|
+
</Message.Content>
|
|
431
|
+
</Message.Root>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</form.Field>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
<DocMessage>
|
|
439
|
+
Gate the message on `field.state.meta.isTouched` if you only want it to appear after the user has visited the field.
|
|
440
|
+
</DocMessage>
|
|
441
|
+
|
|
442
|
+
## Array fields
|
|
443
|
+
|
|
444
|
+
For a repeatable set of fields, such as environment variables, describe the item shape as an array in the schema and declare the field with `mode="array"`:
|
|
445
|
+
|
|
446
|
+
```tsx title="schema" showLineNumbers
|
|
447
|
+
const schema = z.object({
|
|
448
|
+
envVars: z.array(
|
|
449
|
+
z.object({
|
|
450
|
+
key: z.string().min(1, 'Key is required.'),
|
|
451
|
+
value: z.string().min(1, 'Value is required.')
|
|
452
|
+
})
|
|
453
|
+
)
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
An array field exposes `pushValue` and `removeValue`. Each input targets its row with a nested `form.Field` whose `name` includes the index:
|
|
458
|
+
|
|
459
|
+
```tsx title="Array fields" showLineNumbers {1,4,17,20}
|
|
460
|
+
<form.Field name="envVars" mode="array">
|
|
461
|
+
{(arrayField) => (
|
|
462
|
+
<div className="flex flex-col gap-3">
|
|
463
|
+
{arrayField.state.value.map((_, i) => (
|
|
464
|
+
<div key={i} className="flex items-start gap-2">
|
|
465
|
+
<form.Field name={`envVars[${i}].key`}>{(field) => <InputText value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} placeholder="KEY" />}</form.Field>
|
|
466
|
+
<form.Field name={`envVars[${i}].value`}>{(field) => <InputText value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} placeholder="value" />}</form.Field>
|
|
467
|
+
<Button type="button" severity="secondary" variant="text" onClick={() => arrayField.removeValue(i)}>
|
|
468
|
+
<Times />
|
|
469
|
+
</Button>
|
|
470
|
+
</div>
|
|
471
|
+
))}
|
|
472
|
+
<Button type="button" variant="outlined" onClick={() => arrayField.pushValue({ key: '', value: '' })}>
|
|
473
|
+
Add variable
|
|
474
|
+
</Button>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</form.Field>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Nested objects
|
|
481
|
+
|
|
482
|
+
Group related fields under an object in the schema and target them with a dotted `name`:
|
|
483
|
+
|
|
484
|
+
```tsx title="schema"
|
|
485
|
+
const schema = z.object({
|
|
486
|
+
build: z.object({
|
|
487
|
+
command: z.string().min(1, 'Build command is required.'),
|
|
488
|
+
install: z.string().min(1, 'Install command is required.')
|
|
489
|
+
})
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```tsx title="Nested fields"
|
|
494
|
+
<form.Field name="build.command">
|
|
495
|
+
{(field) => <InputText value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} placeholder="npm run build" />}
|
|
496
|
+
</form.Field>
|
|
497
|
+
<form.Field name="build.install">
|
|
498
|
+
{(field) => <InputText value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} placeholder="npm install" />}
|
|
499
|
+
</form.Field>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Defaults follow the same shape: `defaultValues: { build: { command: '', install: '' } }`.
|