mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-27 14:44:12 +00:00
* feat: add Formisch (form library) examples to docs * docs: update Formisch docs with additional form methods and examples * chore: update valibot dependency to version 1.4.0 * style(docs): format formisch imports --------- Co-authored-by: shadcn <m@shadcn.com>
670 lines
21 KiB
Plaintext
670 lines
21 KiB
Plaintext
---
|
|
title: Formisch
|
|
description: Build forms in React using Formisch and Valibot.
|
|
links:
|
|
doc: https://formisch.dev
|
|
---
|
|
|
|
import { InfoIcon } from "lucide-react"
|
|
|
|
This guide covers building forms with [Formisch](https://formisch.dev), the lightweight, schema-first, and fully type-safe form library for React. We'll create forms with the `<Field />` component, validate them with Valibot schemas, handle errors, and ensure accessibility.
|
|
|
|
## Demo
|
|
|
|
We'll build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
|
|
|
<Callout icon={<InfoIcon />}>
|
|
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
|
validation to show how schema validation and form errors work in Formisch. It
|
|
is recommended to add basic browser validation in your production code.
|
|
</Callout>
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-demo"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
## Approach
|
|
|
|
This form leverages Formisch for headless, schema-first form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
|
|
|
- Uses Formisch's `useForm` hook for form state management.
|
|
- `<Form />` component to wrap the native `<form>` element with submit handling.
|
|
- `<Field />` render-prop component for controlled inputs.
|
|
- Schema validation using [Valibot](https://valibot.dev).
|
|
- Type-safe field paths inferred from the schema.
|
|
|
|
## Form Methods
|
|
|
|
Formisch exposes form operations as **top-level functions** rather than methods on a form object. Import only what you need:
|
|
|
|
```ts
|
|
import { getInput, insert, reset, submit } from "@formisch/react"
|
|
```
|
|
|
|
Every method follows the same signature: the **first parameter is always the form store**, and the **second parameter (if necessary) is always a config object**.
|
|
|
|
```ts
|
|
// Read a field value
|
|
const email = getInput(form, { path: ["email"] })
|
|
|
|
// Reset the form with new initial values
|
|
reset(form, { initialInput: { email: "", password: "" } })
|
|
|
|
// Move an item in a field array
|
|
move(form, { path: ["items"], from: 0, to: 3 })
|
|
```
|
|
|
|
This design keeps the API flexible and consistent across all methods. You'll see the same `(form, config)` shape used throughout this guide for reading state (`getInput`, `getErrors`), writing state (`setInput`, `setErrors`), form control (`submit`, `validate`, `focus`), and array operations (`insert`, `remove`, `move`, `swap`, `replace`). See the [full methods reference](https://formisch.dev/react/guides/form-methods) for details.
|
|
|
|
## Anatomy
|
|
|
|
Here's a basic example of a form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
|
|
|
```tsx showLineNumbers {3-21}
|
|
<Form of={form} onSubmit={handleSubmit}>
|
|
<FieldGroup>
|
|
<FormischField of={form} path={["title"]}>
|
|
{(field) => (
|
|
<Field data-invalid={field.errors !== null}>
|
|
<FieldLabel htmlFor="form-title">Bug Title</FieldLabel>
|
|
<Input
|
|
{...field.props}
|
|
id="form-title"
|
|
value={field.input}
|
|
aria-invalid={field.errors !== null}
|
|
placeholder="Login button not working on mobile"
|
|
autoComplete="off"
|
|
/>
|
|
<FieldDescription>
|
|
Provide a concise title for your bug report.
|
|
</FieldDescription>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
</FieldGroup>
|
|
</Form>
|
|
```
|
|
|
|
<Callout icon={<InfoIcon />}>
|
|
**Note:** Formisch ships its own `Field` component. To avoid a name clash with
|
|
the shadcn `Field`, the examples below import the Formisch one as
|
|
`FormischField` and keep the shadcn `Field` under its original name. In your
|
|
own code you can alias either side — just be consistent.
|
|
</Callout>
|
|
|
|
## Form
|
|
|
|
### Create a form schema
|
|
|
|
We'll start by defining the shape of our form using a Valibot schema. Formisch infers all input and output types directly from this schema.
|
|
|
|
```tsx showLineNumbers title="form.tsx"
|
|
import * as v from "valibot"
|
|
|
|
const FormSchema = v.object({
|
|
title: v.pipe(
|
|
v.string(),
|
|
v.minLength(5, "Bug title must be at least 5 characters."),
|
|
v.maxLength(32, "Bug title must be at most 32 characters.")
|
|
),
|
|
description: v.pipe(
|
|
v.string(),
|
|
v.minLength(20, "Description must be at least 20 characters."),
|
|
v.maxLength(100, "Description must be at most 100 characters.")
|
|
),
|
|
})
|
|
```
|
|
|
|
### Set up the form
|
|
|
|
Next, we'll use the `useForm` hook from Formisch to create our form instance. The schema is passed directly to `useForm` — there is no resolver step.
|
|
|
|
```tsx showLineNumbers title="form.tsx" {1-2,21-25}
|
|
import { Form, Field as FormischField, useForm } from "@formisch/react"
|
|
import type { SubmitHandler } from "@formisch/react"
|
|
import * as v from "valibot"
|
|
|
|
const FormSchema = v.object({
|
|
title: v.pipe(
|
|
v.string(),
|
|
v.minLength(5, "Bug title must be at least 5 characters."),
|
|
v.maxLength(32, "Bug title must be at most 32 characters.")
|
|
),
|
|
description: v.pipe(
|
|
v.string(),
|
|
v.minLength(20, "Description must be at least 20 characters."),
|
|
v.maxLength(100, "Description must be at most 100 characters.")
|
|
),
|
|
})
|
|
|
|
export function BugReportForm() {
|
|
const form = useForm({
|
|
schema: FormSchema,
|
|
initialInput: {
|
|
title: "",
|
|
description: "",
|
|
},
|
|
})
|
|
|
|
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
|
// Do something with the validated form values.
|
|
console.log(output)
|
|
}
|
|
|
|
return (
|
|
<Form of={form} onSubmit={handleSubmit}>
|
|
{/* ... */}
|
|
{/* Build the form here */}
|
|
{/* ... */}
|
|
</Form>
|
|
)
|
|
}
|
|
```
|
|
|
|
The `<Form />` component wraps a native `<form>` element. It calls `event.preventDefault()`, runs validation, and only invokes `onSubmit` when the data is valid. The `output` you receive is fully typed from the schema.
|
|
|
|
### Build the form
|
|
|
|
We can now build the form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
|
|
|
<ComponentSource
|
|
src="/registry/new-york-v4/examples/form-formisch-demo.tsx"
|
|
title="form.tsx"
|
|
/>
|
|
|
|
### Done
|
|
|
|
That's it. You now have a fully accessible form with client-side validation.
|
|
|
|
When you submit the form, the `handleSubmit` function will be called with the validated form data. If the form data is invalid, Formisch will populate `field.errors` for each invalid field and the UI will display them.
|
|
|
|
## Validation
|
|
|
|
### Client-side Validation
|
|
|
|
Formisch validates your form data using the Valibot schema you pass to `useForm`. There is no resolver — the schema is the single source of truth for both runtime validation and static types.
|
|
|
|
```tsx showLineNumbers title="form.tsx" {1,3-6,11}
|
|
import { useForm } from "@formisch/react"
|
|
|
|
const FormSchema = v.object({
|
|
title: v.string(),
|
|
description: v.optional(v.string()),
|
|
})
|
|
|
|
export function ExampleForm() {
|
|
const form = useForm({
|
|
schema: FormSchema,
|
|
initialInput: {
|
|
title: "",
|
|
description: "",
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
### Validation Modes
|
|
|
|
Formisch separates the **first** validation from **subsequent** validations. You configure them with the `validate` and `revalidate` options on `useForm`.
|
|
|
|
```tsx showLineNumbers title="form.tsx" {3-4}
|
|
const form = useForm({
|
|
schema: FormSchema,
|
|
validate: "blur",
|
|
revalidate: "input",
|
|
})
|
|
```
|
|
|
|
| Option | Value | Description |
|
|
| ------------ | ----------- | --------------------------------------------------------------- |
|
|
| `validate` | `"submit"` | Validate on form submission (default). |
|
|
| `validate` | `"blur"` | Validate when a field loses focus. |
|
|
| `validate` | `"input"` | Validate on every input change. |
|
|
| `validate` | `"initial"` | Validate immediately on form creation. |
|
|
| `revalidate` | `"input"` | Revalidate on every input change after the first run (default). |
|
|
| `revalidate` | `"blur"` | Revalidate on blur after the first run. |
|
|
| `revalidate` | `"submit"` | Revalidate only on form submission. |
|
|
|
|
## Displaying Errors
|
|
|
|
Display errors next to the field using `<FieldError />`. Formisch returns errors as an array of strings, so map them to the shape `<FieldError />` expects. For styling and accessibility:
|
|
|
|
- Add the `data-invalid` prop to the `<Field />` component.
|
|
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
|
|
|
```tsx showLineNumbers title="form.tsx" {3,10,12-14}
|
|
<FormischField of={form} path={["email"]}>
|
|
{(field) => (
|
|
<Field data-invalid={field.errors !== null}>
|
|
<FieldLabel htmlFor="form-email">Email</FieldLabel>
|
|
<Input
|
|
{...field.props}
|
|
id="form-email"
|
|
value={field.input}
|
|
type="email"
|
|
aria-invalid={field.errors !== null}
|
|
/>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
## Working with Different Field Types
|
|
|
|
Formisch exposes two ways to bind a field to an element:
|
|
|
|
- **Native HTML elements** (like `<Input />` and `<Textarea />`) — spread `field.props` and provide `value={field.input}`. Formisch wires up `name`, `ref`, `onChange`, `onBlur`, and `onFocus` for you.
|
|
- **Component-library inputs** (like Radix-based `<Select />`, `<Checkbox />`, `<RadioGroup />`, `<Switch />`) — read the value from `field.input` and call `field.onChange(value)` to update it.
|
|
|
|
### Input
|
|
|
|
- For input fields, spread `field.props` and provide `value={field.input}`.
|
|
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-input"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {5-8}
|
|
<FormischField of={form} path={["username"]}>
|
|
{(field) => (
|
|
<Field data-invalid={field.errors !== null}>
|
|
<FieldLabel htmlFor="form-username">Username</FieldLabel>
|
|
<Input
|
|
{...field.props}
|
|
id="form-username"
|
|
value={field.input}
|
|
aria-invalid={field.errors !== null}
|
|
/>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Textarea
|
|
|
|
- For textarea fields, spread `field.props` and provide `value={field.input}`.
|
|
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-textarea"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {7-10}
|
|
<FormischField of={form} path={["about"]}>
|
|
{(field) => (
|
|
<Field data-invalid={field.errors !== null}>
|
|
<FieldLabel htmlFor="form-about">More about you</FieldLabel>
|
|
<Textarea
|
|
{...field.props}
|
|
id="form-about"
|
|
value={field.input}
|
|
aria-invalid={field.errors !== null}
|
|
placeholder="I'm a software engineer..."
|
|
className="min-h-[120px]"
|
|
/>
|
|
<FieldDescription>
|
|
Tell us more about yourself. This will be used to help us personalize
|
|
your experience.
|
|
</FieldDescription>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Select
|
|
|
|
- For select components, read `field.input` and call `field.onChange` from `<Select />`'s `onValueChange`.
|
|
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-select"
|
|
className="sm:[&_.preview]:h-[500px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {15-19}
|
|
<FormischField of={form} path={["language"]}>
|
|
{(field) => (
|
|
<Field orientation="responsive" data-invalid={field.errors !== null}>
|
|
<FieldContent>
|
|
<FieldLabel htmlFor="form-language">Spoken Language</FieldLabel>
|
|
<FieldDescription>
|
|
For best results, select the language you speak.
|
|
</FieldDescription>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</FieldContent>
|
|
<Select value={field.input} onValueChange={field.onChange}>
|
|
<SelectTrigger
|
|
id="form-language"
|
|
aria-invalid={field.errors !== null}
|
|
className="min-w-[120px]"
|
|
>
|
|
<SelectValue placeholder="Select" />
|
|
</SelectTrigger>
|
|
<SelectContent position="item-aligned">
|
|
<SelectItem value="auto">Auto</SelectItem>
|
|
<SelectItem value="en">English</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Checkbox
|
|
|
|
- For checkbox arrays, read `field.input` and update it from `onCheckedChange` using `field.onChange`.
|
|
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
|
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-checkbox"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {16,19-25}
|
|
<FormischField of={form} path={["tasks"]}>
|
|
{(field) => (
|
|
<FieldSet>
|
|
<FieldLegend variant="label">Tasks</FieldLegend>
|
|
<FieldDescription>
|
|
Get notified when tasks you've created have updates.
|
|
</FieldDescription>
|
|
<FieldGroup data-slot="checkbox-group">
|
|
{tasks.map((task) => (
|
|
<Field
|
|
key={task.id}
|
|
orientation="horizontal"
|
|
data-invalid={field.errors !== null}
|
|
>
|
|
<Checkbox
|
|
id={`form-checkbox-${task.id}`}
|
|
aria-invalid={field.errors !== null}
|
|
checked={field.input?.includes(task.id) ?? false}
|
|
onCheckedChange={(checked) => {
|
|
const current = field.input ?? []
|
|
field.onChange(
|
|
checked === true
|
|
? [...current, task.id]
|
|
: current.filter((value) => value !== task.id)
|
|
)
|
|
}}
|
|
/>
|
|
<FieldLabel
|
|
htmlFor={`form-checkbox-${task.id}`}
|
|
className="font-normal"
|
|
>
|
|
{task.label}
|
|
</FieldLabel>
|
|
</Field>
|
|
))}
|
|
</FieldGroup>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</FieldSet>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Radio Group
|
|
|
|
- For radio groups, read `field.input` and call `field.onChange` from `onValueChange`.
|
|
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-radiogroup"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {9-13,21}
|
|
<FormischField of={form} path={["plan"]}>
|
|
{(field) => (
|
|
<FieldSet>
|
|
<FieldLegend>Plan</FieldLegend>
|
|
<FieldDescription>
|
|
You can upgrade or downgrade your plan at any time.
|
|
</FieldDescription>
|
|
<RadioGroup value={field.input} onValueChange={field.onChange}>
|
|
{plans.map((plan) => (
|
|
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
|
|
<Field
|
|
orientation="horizontal"
|
|
data-invalid={field.errors !== null}
|
|
>
|
|
<FieldContent>
|
|
<FieldTitle>{plan.title}</FieldTitle>
|
|
<FieldDescription>{plan.description}</FieldDescription>
|
|
</FieldContent>
|
|
<RadioGroupItem
|
|
value={plan.id}
|
|
id={`form-radiogroup-${plan.id}`}
|
|
aria-invalid={field.errors !== null}
|
|
/>
|
|
</Field>
|
|
</FieldLabel>
|
|
))}
|
|
</RadioGroup>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</FieldSet>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Switch
|
|
|
|
- For switches, read `field.input` and call `field.onChange` from `onCheckedChange`.
|
|
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-switch"
|
|
className="sm:[&_.preview]:h-[500px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
```tsx showLineNumbers title="form.tsx" {15-19}
|
|
<FormischField of={form} path={["twoFactor"]}>
|
|
{(field) => (
|
|
<Field orientation="horizontal" data-invalid={field.errors !== null}>
|
|
<FieldContent>
|
|
<FieldLabel htmlFor="form-twoFactor">
|
|
Multi-factor authentication
|
|
</FieldLabel>
|
|
<FieldDescription>
|
|
Enable multi-factor authentication to secure your account.
|
|
</FieldDescription>
|
|
{field.errors && (
|
|
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
|
)}
|
|
</FieldContent>
|
|
<Switch
|
|
id="form-twoFactor"
|
|
checked={field.input ?? false}
|
|
onCheckedChange={field.onChange}
|
|
aria-invalid={field.errors !== null}
|
|
/>
|
|
</Field>
|
|
)}
|
|
</FormischField>
|
|
```
|
|
|
|
### Complex Forms
|
|
|
|
Here is an example of a more complex form with multiple fields and validation.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-complex"
|
|
className="sm:[&_.preview]:h-[1300px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
## Resetting the Form
|
|
|
|
Formisch exposes a top-level `reset` function. Pass the form store to reset it to its initial input.
|
|
|
|
```tsx showLineNumbers
|
|
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
|
Reset
|
|
</Button>
|
|
```
|
|
|
|
You can also reset to new initial values, or reset while keeping the user's current input:
|
|
|
|
```tsx showLineNumbers
|
|
// Reset to a fresh set of initial values
|
|
reset(form, { initialInput: { title: "", description: "" } })
|
|
|
|
// Sync the baseline to new server data, but keep the user's edits
|
|
reset(form, { initialInput: serverData, keepInput: true })
|
|
```
|
|
|
|
## Array Fields
|
|
|
|
Formisch provides a `<FieldArray />` component and a set of helper functions for managing dynamic array fields. Use it whenever you need to add, remove, or reorder items.
|
|
|
|
<ComponentPreview
|
|
name="form-formisch-array"
|
|
className="sm:[&_.preview]:h-[700px]"
|
|
chromeLessOnMobile
|
|
/>
|
|
|
|
### Using FieldArray
|
|
|
|
`<FieldArray />` follows the same render-prop pattern as `<Field />`. Its `items` array contains a stable key per item that you should use as the React `key`.
|
|
|
|
```tsx showLineNumbers title="form.tsx" {1,7-22}
|
|
import {
|
|
Field as FormischField,
|
|
FieldArray,
|
|
insert,
|
|
remove,
|
|
} from "@formisch/react"
|
|
|
|
export function ExampleForm() {
|
|
// ... form config
|
|
|
|
return (
|
|
<FieldArray of={form} path={["emails"]}>
|
|
{(fieldArray) => (
|
|
<FieldGroup className="gap-4">
|
|
{fieldArray.items.map((item, index) => (
|
|
<FormischField
|
|
key={item}
|
|
of={form}
|
|
path={["emails", index, "address"]}
|
|
>
|
|
{(field) => /* ... */}
|
|
</FormischField>
|
|
))}
|
|
</FieldGroup>
|
|
)}
|
|
</FieldArray>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Array Field Structure
|
|
|
|
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
|
|
|
|
```tsx showLineNumbers title="form.tsx"
|
|
<FieldSet className="gap-4">
|
|
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
|
<FieldDescription>
|
|
Add up to 5 email addresses where we can contact you.
|
|
</FieldDescription>
|
|
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
|
|
</FieldSet>
|
|
```
|
|
|
|
### Adding Items
|
|
|
|
Use the `insert` function to add new items to the array. By default new items are appended to the end. You can also pass an `at` index to insert at a specific position.
|
|
|
|
```tsx showLineNumbers title="form.tsx"
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() =>
|
|
insert(form, { path: ["emails"], initialInput: { address: "" } })
|
|
}
|
|
disabled={fieldArray.items.length >= 5}
|
|
>
|
|
Add Email Address
|
|
</Button>
|
|
```
|
|
|
|
### Removing Items
|
|
|
|
Use the `remove` function with an `at` index to remove items from the array.
|
|
|
|
```tsx showLineNumbers title="form.tsx"
|
|
import { remove } from "@formisch/react"
|
|
|
|
{
|
|
fieldArray.items.length > 1 && (
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupButton
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => remove(form, { path: ["emails"], at: index })}
|
|
aria-label={`Remove email ${index + 1}`}
|
|
>
|
|
<XIcon />
|
|
</InputGroupButton>
|
|
</InputGroupAddon>
|
|
)
|
|
}
|
|
```
|
|
|
|
Formisch also exposes `move`, `swap`, and `replace` for reordering and replacing items. They follow the same `(form, config)` signature.
|
|
|
|
### Array Validation
|
|
|
|
Use Valibot's `array` and pipeline validators to constrain array fields.
|
|
|
|
```tsx showLineNumbers title="form.tsx"
|
|
const FormSchema = v.object({
|
|
emails: v.pipe(
|
|
v.array(
|
|
v.object({
|
|
address: v.pipe(
|
|
v.string(),
|
|
v.nonEmpty("Enter an email address."),
|
|
v.email("Enter a valid email address.")
|
|
),
|
|
})
|
|
),
|
|
v.minLength(1, "Add at least one email address."),
|
|
v.maxLength(5, "You can add up to 5 email addresses.")
|
|
),
|
|
})
|
|
```
|