Fields
The four shapes of a field entry, nested objects, and TanStack's options per field.
fields describes what a form contains, and its shape follows the data: an object in the data is
an object in the config, an array is an array. Each entry under fields has one of four shapes,
marked by one key:
| Shape | Marker | For |
|---|---|---|
| Leaf | widget | one value, rendered by a widget |
| Object | fields | a value with parts |
| Array | item | a list, one entry per item |
| Custom | render | a field you render yourself |
The shapes exclude each other. An entry with a widget and fields does not compile, so an entry
never half-works.
How a level renders
Fields walks a level and renders each entry at its path, the name TanStack knows the field
by: email, address.zip, contacts[0].name. You never write a path in a config; formdef builds
it while it descends.
Leaf
A leaf names a widget and passes it its props:
export const = createForm({
: .({ : .() }),
: { : '' },
fields: {
email: { : 'text', : { : 'Email', : 'you@example.com' } },
},
})| Key | Meaning |
|---|---|
widget | The registered widget that renders the field. It must serve the field's value type. |
props | The widget's own props, checked against that widget. Required when the widget has a prop it cannot render without. |
props may also be a function of the form's values; Visibility and derived
props shows how.
A widget that takes required can only receive required: false where the schema makes the key
optional, so a config never claims a value the schema does not demand:
export const = createForm({
: .({ : .().(1), : .().() }),
: { : '' },
fields: {
name: { : 'text', : { : 'Name', : true } },
nickname: { : 'text', props: { : 'Nickname', : true } }, },
})Object
An object entry describes the parts of a value with fields of its own:
const = .({
: .(),
: .({ : .(), : .() }),
})
export const = createForm({
,
: { : '', : { : '', : '' } },
fields: {
name: { : 'text', : { : 'Name' } },
address: {
: {
zip: { : 'text', : { : 'ZIP code' } },
city: { : 'text', : { : 'City' } },
},
},
},
})The inputs land at address.zip and address.city. An object renders no markup of its own: its
fields appear where the object sits. To draw a box around them, use a
section in the layout. Each object is a level and may carry
its own layout, which names the keys of that level only.
An object entry may carry TanStack's field options too, for a rule over the whole object. formdef then mounts a field at the object's path, so the rule runs; without options it mounts none.
export const = createForm({
,
: { : { : '', : '' } },
fields: {
address: {
: {
: ({ }) =>
. !== '' && . === '' ? 'A ZIP code needs a city' : ,
: ['address.zip', 'address.city'],
},
: {
zip: { : 'text', : { : 'ZIP code' } },
city: { : 'text', : { : 'City' } },
},
},
},
})onChangeListenTo makes the rule run when a part changes: TanStack validates the field whose value
was set, and setting address.zip alone does not run a rule on address.
Leaving keys out
A key of the data that fields does not describe is not rendered. It keeps its value, the schema
still validates it, and it is submitted, which suits ids and values the server manages. A key the
data does not have is a compile error.
One widget for a whole array
A list that is one control, such as tags or a multi-select, is a leaf whose widget serves the
array type, FieldOf<string[]>:
export const = createForm({
: .({ : .(.()) }),
: { tags: [] },
fields: { tags: { : 'tags', : { : 'Tags' } } },
})An array whose items each get their own fields is an array entry; Arrays covers it.
Custom
A custom entry hands you TanStack's field, typed with the field's value, and renders what you return:
export const = createForm({
: .({ : .(), : .() }),
: { : '', : '' },
fields: {
name: { : 'text', : { : 'Name' } },
avatar: {
: () => (
< ={.state.} ={() => .()} />
),
},
},
})Use it for a control that does not fit the widget contract, or for one form only. It is what you would write against TanStack directly, inside a config.
TanStack's options per field
Every entry, whatever its shape, takes TanStack's field options and passes them on unchanged:
validators, listeners, asyncDebounceMs, asyncAlways, defaultValue, defaultMeta,
disableErrorFlat and mode. An array entry takes all of them except mode: formdef renders it
in TanStack's array mode, so the list renders again when items are added, removed or moved, not
when one of them changes.
export const = createForm({
: .({ : .().(3) }),
: { : '' },
fields: {
username: {
: 'text',
: { : 'Username' },
: 300,
: {
: async ({ }) =>
(await ()) ? 'This username is taken' : ,
},
},
},
})Validators and listeners on an entry are typed against that field's value, and they run next to the schema.