Skip to content

Lend a form's fields

An assisted field is a field on an assisted page the assistant fills as the user would type into it. Each field is a tool of its name, and the page’s note shows what each field says now. Render it inside an AssistedPage. Outside one, it throws.

A fill changes the form, not your data. Keep saving on the page’s own button, lent as an action.

Wrap the input, textarea, native select, checkbox or switch in AssistedInput with a name and a label. The label says what the field holds. From examples/app/app/orders/[id]/page.tsx:

<AssistedInput name="note" label="The order's note">
<Textarea value={draft} onChange={(event) => setDraft(event.target.value)} />
</AssistedInput>

AssistedInput takes one child and attaches a ref to it. The child must pass that ref to the field’s DOM element. shadcn’s Input, Textarea and NativeSelect do.

A fill writes through the element’s own value setter and fires an input event, so your onChange runs as if the user typed. Uncontrolled forms see the new value too. Once the page has rendered, the fill answers Filled "<label>"; it now says <value>.

Give every field the same name and its own id. The assistant sees one tool that fills a field by id. From examples/app/app/settings/page.tsx:

{fields.map(([field, id, label]) => (
<Label key={field} className="grid gap-1.5">
{label}
<AssistedInput name="setting" id={id} label={label}>
<Input value={draft[field]} onChange={(event) => set(field, event.target.value)} />
</AssistedInput>
</Label>
))}

The page’s note then lists the group with each field’s value:

Its fields: setting (name: Shop name, now "Northwind"; currency: Currency, now "USD"; email: Notification email, now "alerts@northwind.example").

Say in the page’s instructions which action saves the form, so the assistant fills and then saves.

AssistedInput reads the wrapped element when it mounts and fills it by its kind:

Child A fill The note shows
<input>, <textarea> Sets the value, fires input The text, cut at 200 characters
<input type="password"> Sets the value, fires input (filled) or (empty)
Checkbox, or role="switch" Clicks it when it is not already on or off as asked on or off
<select> Sets the value, fires change The selected value
Anything else Rings it for the user Its text

A select’s options become the only values its tool takes, read from the DOM when the field mounts. A switch or checkbox takes on and off.

Pass consequential when the user should type the value themselves. A call then rings the field and answers that filling it is theirs:

<AssistedInput name="email" label="Your email" consequential>
<Input type="email" name="email" />
</AssistedInput>

A disabled or read-only field answers that it is disabled or read-only right now, and nothing changes.

A custom control keeps its value in state, not in a DOM field. Lend it with AssistedField, which takes the value and a function that fills it. options lists the only values it takes:

const plans = [
{ value: "free", label: "Free" },
{ value: "pro", label: "Pro" },
];
export function PlanPicker({ plan, setPlan }: { plan: string; setPlan: (plan: string) => void }) {
return (
<AssistedField name="plan" label="Billing plan" options={plans} value={plan} onFill={setPlan}>
<div role="radiogroup" aria-label="Billing plan">
{plans.map((option) => (
<button
key={option.value}
type="button"
role="radio"
aria-checked={plan === option.value}
onClick={() => setPlan(option.value)}
>
{option.label}
</button>
))}
</div>
</AssistedField>
);
}

The child is only the ring target. onFill gets the value the assistant chose, and the answer shows value once the page has rendered.

When wrapping does not fit, call useAssistedInput and attach the ref it returns:

export function CouponInput() {
const ref = useAssistedInput<HTMLInputElement>({ name: "coupon", label: "Coupon code" });
return <input ref={ref} name="coupon" />;
}

Open the devtools’ Assisted pages tab. The Fields list shows each field tool with what every field says now. See Mount the devtools.