Combobox field
One choice among many options, filtered by what is typed.
Installation
npx shadcn@latest add @formdef/combobox-fieldRegister it under widgets in lib/formdef/index.tsx, as widgets: { combobox: ComboboxField }.
The widget serves string values.
Usage
export const = createForm({
: .({ : .().(1, 'Choose a time zone') }),
: { : '' },
fields: {
timeZone: {
: 'combobox',
: {
: 'Time zone',
: 'Search time zones',
: true,
: 'No time zone matches.',
: [
{ : 'Europe/Berlin', : 'Berlin' },
{ : 'Asia/Tokyo', : 'Tokyo' },
],
},
},
},
})Typing filters the options by their label; the value is the chosen option's value, and '' stands
for no choice. A value the options do not carry stays in the form and shows as it is.
| Prop | Meaning |
|---|---|
empty | The text shown when nothing matches. Defaults to No results. |
showClear | Shows a button that clears the choice. |
clearLabel | The accessible name of that button. Defaults to Clear. |
Props
Prop
Type
Source
import { Combobox as ComboboxPrimitive } from '@base-ui/react'
import { type FieldOf, fieldParts } from '@formdef/core'
import { XIcon } from 'lucide-react'
import type { ComponentProps } from 'react'
import { FieldFrame } from '@/components/formdef/field-frame'
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '@/components/ui/combobox'
import { InputGroupAddon, InputGroupButton } from '@/components/ui/input-group'
import type { FieldLabels } from '@/lib/formdef/field-labels'
import type { Option } from '@/lib/formdef/options'
type Attrs = Pick<ComponentProps<typeof Combobox>, 'disabled' | 'readOnly' | 'required'> &
Pick<ComponentProps<typeof ComboboxInput>, 'className' | 'placeholder' | 'showClear'>
type ComboboxFieldProps = FieldOf<string> &
FieldLabels &
Attrs & {
options: Option[]
empty?: string | undefined
clearLabel?: string | undefined
}
const NO_RESULTS = 'No results.'
const CLEAR = 'Clear'
export const ComboboxField = ({
field,
label,
labelAside,
description,
options,
empty = NO_RESULTS,
clearLabel = CLEAR,
className,
placeholder,
showClear,
...attrs
}: ComboboxFieldProps) => {
const parts = fieldParts(field, { description })
const { name: _carriedByTheForm, ...input } = parts.control
const value = field.state.value ?? ''
const unknown: Option | null = value === '' ? null : { value, label: value }
const selected = options.find((option) => option.value === value) ?? unknown
return (
<FieldFrame
parts={parts}
label={label}
labelAside={labelAside}
description={description}
disabled={attrs.disabled}
>
<Combobox
{...attrs}
items={options}
value={selected}
onValueChange={(option) => field.handleChange(option?.value ?? '')}
itemToStringLabel={(option) => option.label}
itemToStringValue={(option) => option.value}
isItemEqualToValue={(item, chosen) => item.value === chosen.value}
>
<ComboboxInput
{...input}
className={className}
disabled={attrs.disabled}
placeholder={placeholder}
>
{showClear === true && (
<InputGroupAddon align="inline-end">
<ComboboxPrimitive.Clear
aria-label={clearLabel}
disabled={attrs.disabled}
render={<InputGroupButton variant="ghost" size="icon-xs" />}
>
<XIcon />
</ComboboxPrimitive.Clear>
</InputGroupAddon>
)}
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>{empty}</ComboboxEmpty>
<ComboboxList>
{(option: Option) => (
<ComboboxItem key={option.value} value={option} disabled={option.disabled}>
{option.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</FieldFrame>
)
}