Implementing a select component with conform #376
-
I'm trying to implement a select field component by following the same structure as the checkbox field. It works fine, however, re-validating on-blur seems to be an issue. I'd also be interested on how to do this with a combobox as well. Here is the component: export function SelectField({
labelProps,
buttonProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
buttonProps: SelectProps
errors?: ListOfErrors
className?: string
}) {
const [open, setOpen] = React.useState(false)
const fallbackId = useId()
const buttonRef = useRef<HTMLButtonElement>(null)
const control = useInputEvent({
ref: () =>
buttonRef.current?.form?.elements.namedItem(buttonProps.name ?? ''),
onFocus: () => buttonRef.current?.focus(),
})
const id = buttonProps.id ?? buttonProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Select name={buttonProps.name} open={open} onOpenChange={setOpen}>
<SelectTrigger
id={id}
ref={buttonRef}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...buttonProps}
onChange={state => {
control.change(state.currentTarget.value)
buttonProps.onChange?.(state)
}}
onFocus={event => {
control.focus()
buttonProps.onFocus?.(event)
}}
onBlur={event => {
control.blur()
buttonProps.onBlur?.(event)
}}
type="button"
className="w-[180px]"
>
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
// These values will be passed as children later
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
</SelectContent>
</Select>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
} |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
You may need to ask this on the Conform repo. I'm not sure. |
Beta Was this translation helpful? Give feedback.
-
The issue here is caused by the To fix it, just pick the export function SelectField() {
// ...
const { name, ...props } = buttonProps;
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Select name={buttonProps.name} open={open} onOpenChange={setOpen} aria-invalid={true}>
<SelectTrigger
{...props}
{/* ... */}
>
<SelectValue placeholder="Theme" />
</SelectTrigger>
</Select>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
</SelectContent>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
);
} |
Beta Was this translation helpful? Give feedback.
-
select.tsx export type SelectProps = Omit<
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>,
'type'
> & {
type?: string
} forms.tsx export function SelectField({
labelProps,
buttonProps,
errors,
className,
children,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
buttonProps: SelectProps
errors?: ListOfErrors
className?: string
children: React.ReactNode
}) {
const [open, setOpen] = React.useState(false)
const fallbackId = useId()
const buttonRef = useRef<HTMLButtonElement>(null)
const control = useInputEvent({
ref: () =>
buttonRef.current?.form?.elements.namedItem(buttonProps.name ?? ''),
onFocus: () => buttonRef.current?.focus(),
onBlur: () => buttonRef.current?.blur(),
})
const id = buttonProps.id ?? buttonProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
const { name, ...props } = buttonProps
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Select
name={buttonProps.name}
open={open}
onOpenChange={setOpen}
defaultValue={buttonProps.defaultValue?.toString()}
>
<SelectTrigger
id={id}
ref={buttonRef}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...props}
onChange={state => {
control.change(state.currentTarget.value)
buttonProps.onChange?.(state)
}}
onFocus={event => {
control.focus()
buttonProps.onFocus?.(event)
}}
onBlur={event => {
control.blur()
buttonProps.onBlur?.(event)
}}
type="button"
>
<SelectValue placeholder={labelProps.children} />
</SelectTrigger>
<SelectContent>{children}</SelectContent>
</Select>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
} I added |
Beta Was this translation helpful? Give feedback.
The issue here is caused by the
name
props passed to the<SelectTrigger />
when you spread thebuttonProps
on it. This makes both the button element and select element having the samename
set and so it fails to get a correctref
when you dobuttonRef.current?.form?.elements.namedItem(buttonProps.name ?? '')
. It would be easier to spot this if Conform can warn about it.To fix it, just pick the
name
prop out of thebuttonProps
: