Arrays
Lists with one entry per item, their controls, and the frame around them.
An array entry expands a list: each item gets the entry you describe under item, at its own
path.
const = .({
: .(.({ : .(), : .() })),
})
export const = createForm({
,
: { contacts: [] },
fields: {
contacts: {
: () => ({ : '', : '' }),
: 'Add contact',
: (, ) => `Remove ${. || `contact ${ + 1}`}`,
: {
: {
name: { : 'text', : { : 'Name' } },
email: { : 'text', : { : 'Email' } },
},
},
},
},
})The inputs land at contacts[0].name, contacts[0].email, contacts[1].name and so on.
| Key | Required | Meaning |
|---|---|---|
item | yes | The entry for one item, in any of the four shapes. |
newItem | yes | Builds a new, empty item. The schema does not say what empty means, so you do. |
addLabel | yes | The accessible name of the add control. |
removeLabel | yes | The accessible name of each remove control. A function of the item and its index names what it removes. |
itemKey | no | The identity of an item, so its widgets keep their state when the list is reordered. |
props | when the frame requires them | Props for the registered array frame. |
Name each remove control after what it removes. Three buttons all called "Remove" tell a screen reader nothing about which contact goes.
Keys and reordering
Without itemKey, items are keyed by their position. Values always follow their item, but
what a widget holds itself, such as an open popover, focus or an uncontrolled input, stays at the
old position when items move. Give items an identity as soon as they can be reordered:
const = .({ : .(.({ : .(), : .() })) })
export const = createForm({
,
: { steps: [] },
fields: {
steps: {
: () => .,
: () => ({ : .(), : '' }),
: 'Add step',
: (, ) => `Remove step ${ + 1}`,
: { : { title: { : 'text', : { : 'Step' } } } },
},
},
})The default controls
Without a registered frame, formdef renders the items, a remove button after each, and one add
button at the end. There is no wrapper element; the buttons carry data-formdef-remove and
data-formdef-add, so your CSS can reach them:
<!-- the fields of contacts[0] -->
<button type="button" data-formdef-remove>Remove Ada</button>
<!-- the fields of contacts[1] -->
<button type="button" data-formdef-remove>Remove Grace</button>
<button type="button" data-formdef-add>Add contact</button>A frame of your own
A registry takes one array frame under structure.arrayFrame, and it frames every array entry of
every form built on that registry. It receives ArrayFrameProps: the array field, one key per
item, a function that renders an item, the resolved labels and TanStack's array operations.
import type { } from '@formdef/core'
type = { : string }
export const = <,>({
,
,
,
,
,
,
,
}: <> & ) => (
<section aria-label={}>
<ul>
{.((, ) => (
<li ={}>
{()}
<button type="button" onClick={() => ()}>
{[]}
</button>
</li>
))}
</ul>
<button type="button" onClick={}>
{}
</button>
</section>
)Register it, and every array entry passes the frame's own props through props, checked against
the frame:
const { createForm } = ({
,
,
: { : },
: { : },
})
export const = createForm({
: .({ : .(.()) }),
: { members: [] },
fields: {
members: {
: () => '',
: 'Add member',
: () => `Remove ${ || 'member'}`,
: { : 'Team members' },
: { : 'text', : { : 'Name' } },
},
},
})The frame renders again when items are added, removed or moved, when a key or a remove label
changes, and when the list itself gets an error; typing inside an item does not render it again. A
frame that shows values of its items, such as a total, subscribes to them itself, for instance with
TanStack's useStore(field.store, selector).
Besides add and remove, a frame receives insert, move, swap, replace and clear, so it
can offer reordering. insert builds the item with newItem when it gets none.
- Errors about the whole list. A rule on the list itself, such as a minimum length, reports on
the array field. The frame receives it as
fieldand is the only place that can show it. - Props for an item's widget.
renderItem(index, extra)passesextrato the widget of the item, for instance to label each one "Contact 2". This reaches a widget only when the item is a leaf, or an object whose only field is a leaf and which has no layout. Anywhere else there is no single widget to receive it; formdef drops it and warns once in development.
A registry has exactly one frame. An array that needs something else renders itself, with a custom entry on TanStack's array field:
export const = createForm({
: .({ : .(.()) }),
: { links: [] },
fields: {
links: {
: 'array',
: () => (
<div>
<p>{.state..} links</p>
<button type="button" onClick={() => .('https://')}>
Add link
</button>
</div>
),
},
},
})