Forms
Spread the input that @k8ordo/form derives for a field straight onto @k8ordo/ui’s inputs. You also learn which component fits each schema and what else to pass.
On this page
Spreading input
talk-form.tsxconst title = form.field('title');
<FormControl
errorText={title.error}
invalid={title.invalid}
label="Title"
required={title.required}
renderInput={(props) => <TextField {...props} {...title.input} />}
/>form.field(name).input holds the attributes @k8ordo/form derives from the schema, such as type and required. How useForm works is covered in @k8ordo/form’s Get Started.
Give FormControl only label, errorText, invalid and required; it creates the id and the aria-* links itself. Spread input after the props FormControl hands over, with nothing taken out first. TextField renders the date and time types as well, and Textarea drops the type a <textarea> does not have.
Every component works uncontrolled, with its value held in the input element. A reset puts it back to defaultValue, and isDirty compares against defaultValue. A value a component changes in code, such as a stepper or a chosen option, fires an input event the way typing does.
Components by schema
For each schema type, the components that fit and what to pass besides {...props} {...input}.
z.string(),z.email(),z.url(),z.iso.*():TextFieldorTextarea, with nothing else to pass.- A password:
PasswordInput. Its show/hide toggle still works aftertypeis spread. z.coerce.number():NumberFieldorSlider. They accept stringmin,maxandstep="any".z.enum([…]):Select,RadioorRadioCard. Passoptions, andlabelAs="legend"onFormControlfor radios.z.boolean(),z.literal(true):CheckboxorSwitch. Pass alabel; they need noFormControl.z.array(z.enum([…])):CheckboxGroup.Root,CheckboxCardorAutocomplete. PassdefaultValue.z.file():FileField.Root, with aFileField.Triggerand aFileField.ItemListinside.
Choice components
tags-field.tsxconst tags = form.field('tags');
const checked = state.values?.tags;
<FormControl
errorText={tags.error}
invalid={tags.invalid}
label="Tags"
labelAs="legend"
renderInput={(props) => (
<CheckboxCard
{...props}
{...tags.input}
defaultValue={Array.isArray(checked) ? checked : []}
options={options}
/>
)}
/>A checkbox group’s value comes back in state.values as an array. input carries only the name, so pass that array as defaultValue. minChecked applies to Autocomplete too. It submits through a hidden <select multiple> that stays even with nothing selected, and the error is attached to that element. Focus sent there after a failure moves on to the text input.
Spread input onto Radio and RadioCard directly. They read defaultValue as the selected option and put required on every radio.
For required to take effect on a Select, put { value: '', label: '…' } first in options. A <select> without an empty option always has a choice selected.
Caveats
Pitfall
A spread value does not reach the <input> inside Checkbox, so a z.stringbool() field drawn with it submits the browser’s on. An unconfigured z.stringbool() reads on as true. With a custom truthy, the submitted on no longer fits the schema, and it differs from the value @k8ordo/state writes to the URL. Render that field as a plain <input {...field.input} />.
NumberField renders type="text" so it can format and step the value, and the browser does not check min and max on a text input. NumberField reports an out-of-range value itself with setCustomValidity. That message comes from @k8ordo/ui’s dictionary, not from zod. This is the only place the client’s text is not zod’s own.
A regex on a field rendered as a Textarea reaches the markup as pattern, but a <textarea> ignores that attribute. The check runs on the server only. formFields does not know which element renders the field, so dropped cannot list it either.
FileField submits what FileField.ItemList shows. A file removed from the list leaves the submission, and a reset empties the list.
Form-level error
talk-form.tsx{form.formError.message !== undefined && (
<Alert
{...form.formError.props}
message={form.formError.message}
tone="error"
/>
)}form.formError can be shown with Alert. Alert passes id and tabIndex through to its element, so spreading form.formError.props lets it take focus.
Alert has role="alert", so a screen reader may read the message twice: once when it appears, once when focus lands on it. An error summary with an alert role behaves the same way.