Introduction
Describe a form once as a typed object, render it as a React component.
formdef turns a typed config into a form built on TanStack Form. You write which field gets which widget, in which order; formdef renders it, and TanStack does the rest: values, validation, arrays, submission.
import { , type } from '@formdef/core'
import { createFormHookContexts } from '@tanstack/react-form'
import { } from 'zod'
const { , } = createFormHookContexts()
const = ({ , }: <string> & { : string }) => (
<label>
{}
<input value={.state.} onChange={() => .(..)} />
</label>
)
const { createForm } = ({ , , : { : } })
export const = createForm({
: .({ : .() }),
: { : '' },
fields: { email: { : 'text', : { : 'Email' } } },
})<SignIn /> renders a <form> with the email field, validates it with the schema and submits
through TanStack.
What the compiler checks
A widget is checked against the value it will receive. Point a checkbox, which serves a
boolean, at a string field, and the config does not compile:
export const = createForm({
: .({ : .(), : .() }),
: { : '', : false },
fields: {
: { widget: 'checkbox', props: { : 'Email' } }, remember: { : 'checkbox', : { : 'Remember me' } },
},
})The same check covers the props a widget receives, the names a layout writes and the default values. All of it happens at compile time; nothing of it runs in the browser.
formdef adds, it never takes away
Every option TanStack Form takes still reaches it, for the form and for each field. When a form
outgrows one call, you take over the <form> element and keep the config you wrote; when formdef
does not help, you write plain TanStack next to it. Ways in shows the three.