Forms

Conform Async Multiple Select

Async Multiple Select wired to Conform: derives name and validity from field metadata, mirrors the remote-loaded selection into hidden inputs for submission, and renders inline errors.

conformselectmulti-selectasyncformvalidationtagssearch

Bound to a Conform form

Submit without selecting anyone to see the validation error wired from field metadata. Options load from the source as you search.

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import type { FieldMetadata } from "@conform-to/react"
import { useMemo } from "react"
import {
  AsyncMultipleSelect,
  type AsyncMultipleSelectOption,
  type AsyncMultipleSelectProps,
} from "@/components/async-multiple-select"
import { FieldError, Label } from "@/components/field"

interface ConformAsyncMultipleSelectProps<T extends AsyncMultipleSelectOption>
  extends Omit<AsyncMultipleSelectProps<T>, "name" | "defaultValue" | "isInvalid"> {
  // A multi-value select bound to a string array form value. Only
  // name/initialValue/errors are read off the metadata.
  field: FieldMetadata<string[]>
  label?: string
  /**
   * Initial selection as full option objects so tags render labels. Falls back
   * to the field's initial ids (label = id) when omitted — pass this whenever
   * the label differs from the stored id.
   */
  defaultSelected?: T[]
}

/**
 * ConformAsyncMultipleSelect — Async Multiple Select wired to Conform.
 *
 * Binds a multi-value Conform field to the quebi Async Multiple Select: derives
 * name and validity from the field metadata, mirrors the selection into hidden
 * inputs for submission, and renders inline errors. Provide `load` to fetch
 * options and `defaultSelected` to seed labelled tags from the field's initial value.
 */
export function ConformAsyncMultipleSelect<T extends AsyncMultipleSelectOption>({
  field,
  label,
  defaultSelected,
  ...props
}: ConformAsyncMultipleSelectProps<T>) {
  const defaultValue = useMemo<T[]>(() => {
    if (defaultSelected) return defaultSelected
    const initial = (field.initialValue as string[] | undefined) ?? []
    return initial.map((id) => ({ id, name: id }) as T)
  }, [defaultSelected, field.initialValue])

  return (
    <div className="flex flex-col gap-1.5">
      {label && <Label htmlFor={field.id}>{label}</Label>}
      <AsyncMultipleSelect<T>
        {...props}
        name={field.name}
        defaultValue={defaultValue}
        isInvalid={!!field.errors}
      />
      <FieldError>{field.errors?.join(", ")}</FieldError>
    </div>
  )
}