Nasaq

RelationPicker

A field that points at another record and finds it by searching your API, storing ids and showing names, for one record or many, with create from typed text and loading, empty and error states.

PreviewOpen ↗

Code

<RelationPicker  aria-label="Customer"  value={customerId}  onValueChange={setCustomerId}  search={(query, signal) => fetch(`/api/customers?q=${query}`, { signal }).then((r) => r.json())}  resolve={(ids) => fetch(`/api/customers?ids=${ids.join(",")}`).then((r) => r.json())}  onCreate={async (name) => ({ value: await api.createCustomer(name), label: name })}/>

Forms · stable

Live examples and controls: RelationPicker in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/relation-picker.json

A foreign-key field. The value is an id; the person sees a name. Results come from your search function, so the list can be as long as your table. It is built on Combobox, so filtering is Arabic-aware when you pass fixed options instead.

When to use

  • Choosing a customer, project, owner or parent from a table too big for a Select.

When not to use

  • Fewer than about 20 choices: a Select.
  • Free tags: a tags input.

Import

import { RelationPicker } from "@fadymondy/nasaq/web";

Quick start

<RelationPicker
  aria-label="Customer"
  value={customerId}
  onValueChange={setCustomerId}
  search={(query, signal) => fetch(`/api/customers?q=${query}`, { signal }).then((r) => r.json())}
  resolve={(ids) => fetch(`/api/customers?ids=${ids.join(",")}`).then((r) => r.json())}
  onCreate={async (name) => ({ value: await api.createCustomer(name), label: name })}
/>

Add multiple and pass string[] for several records.

Anatomy

RelationPicker            data-slot="relation-picker"
└─ Combobox               input (single) or chips (multiple)
   └─ list                results, the selected records, and a "Create ..." row

API

PropTypeDefaultDescription
value / defaultValue / onValueChangestring | null, or string[] with multiplenoneIds. The callback also gets the record or records.
multiplebooleanfalseChips.
search(query, signal) => Promise<RelationOption[]>noneCalled with an empty query when the list opens, then after debounce. Earlier calls are aborted.
optionsRelationOption[]noneFixed records, searched on the client when search is omitted.
resolve(ids) => Promise<RelationOption[]>noneNames for saved ids the picker has not seen.
onCreate(query) => Promise<RelationOption | { error }>noneAdds a row to create a record from the typed text.
debouncenumber250Milliseconds.
renderOption(option) => ReactNodelabel and description
invalid, disabled, id, name, placeholder, aria-labelname writes hidden inputs for a native form.
locale, labels, className

RelationOption: value, label, labelAr, description, disabled.

Accessibility

  • It is a combobox: type to search, arrow keys move, Enter picks, Escape closes.
  • Loading, empty and error states are announced in a status region; the error has a retry button.
  • Chips have a remove button named "Remove".

RTL & i18n

Names use labelAr when the locale is Arabic. Labels and option text are isolated with bdi dir="auto".

Styling & tokens

Uses Combobox tokens. Extend with className.

Do / Don't

  • Do return a stable value (the id), never the label.
  • Do provide resolve when the form loads saved values.
  • Do not fetch in renderOption.

Lab

https://docs.nasaqui.com/?path=/docs/components-forms-relation-picker--docs

On this page