Lookup
Find and pick a record — one by default, or several with multiple. It fetches nothing itself: it reports what was typed, you run the query and hand the records back.
fandry-lookup
Example
Link to section ExampleCustom colors and parts
Link to section Custom colors and partsThe same component with a few --fd-* tokens set around it and its parts restyled. The code is under Parts below.
Props
Link to section Props| Prop | Type | Default | Description |
|---|---|---|---|
label | string | '' | Visible label. |
placeholder | string | '' | Shown in the empty search box. |
help-text | string | '' | Helper text below the field. |
error-text | string | '' | An error shown under the field: the field is invalid while it has one (aria-invalid, the invalid state), and a screen reader hears it when it appears and when the field is focused. |
name | string | '' | Exposed as data-name on the input. |
results | { id, label, description?, disabled?, … }[] | [] | Matches for the current search, shown exactly as given (never re-filtered). Extra fields ride along and come back in change. |
loading | boolean | false | Shows a searching indicator while your query is in flight. |
multiple | boolean | false | Allow any number of records, shown as removable chips with a “Clear all”. The list stays open after each pick, already-chosen records are not offered again, and Backspace in an empty field removes the last chip. |
debounce-ms | number | 250 | Milliseconds typing must pause before search fires; 0 searches on every keystroke. Opening the list still searches at once. |
value | string | string[] | '' | The selected id (an array of ids with multiple). Set it to render existing data; it updates when the user picks or clears. The lookup names each id from record/records or results; for any it can't, it fires resolve and shows the raw id (muted) until you supply the record. |
record | { id, label, … } | null | null | Single mode: the selected record, shown as the field's value with a clear button. On its own it preselects; once value has been set it only supplies the name for that id. |
records | { id, label, … }[] | [] | Multiple mode: the selected records. On its own it preselects; once value has been set it only supplies names for those ids (any subset is fine). |
required | boolean | false | Marks the field required (asterisk + aria-required). |
disabled | boolean | false | Disables the field and the pill's clear button. |
element-props | Record<string, unknown> | {} | Spread onto the native input; keys the component controls are ignored with a warning. |
search (event) | CustomEvent<{ query }> | — | Fires when the list opens by click or ArrowDown (immediately, so an empty query can offer recent records), after typing pauses, and in multiple mode after each pick. |
resolve (event) | CustomEvent<{ values }> | — | Fires once for ids set through value that the lookup can't name yet. Look them up and set record/records. It does not repeat for an id already asked about. |
change (event) | CustomEvent<{ value, record }> | CustomEvent<{ values, records }> | — | Single mode: { value, record } on pick, and { value: '', record: null } on clear. Multiple mode: { values, records } on every pick, removal and Clear all. |
empty (slot) | slot | 'No records found' | Replaces the message shown when a search has no matches. |
clear-all · searching (slots) | slot | 'Clear all' · 'Searching…' | Replace the Clear all button's text and the searching line. |
messages | { searching, clear(name), remove(name) } | {} | Replaces the accessible names of the spinner and the clear and remove buttons, e.g. to translate them. |
label · help-text · error-text (slots) | slot | the props | Markup in place of the label, help text or error text. Shown even without the matching prop. |
Parts
Link to section PartsStyle the elements inside the component from your own CSS with ::part(). Every fandry component renders in native shadow DOM, so parts work on LWR and Salesforce alike. For colors, spacing, radius and type across many components, set the --fd-* tokens instead. The code for the "Custom colors and parts" example above:
| Part | Description |
|---|---|
base | The outermost element. |
label | The label. |
required | The required-field asterisk. |
control | The box the field draws: a text field's border, a select's button, a checkbox's box, a radio's circle, a switch's track. |
selection | The chosen record, in single-select mode. |
selection-label | The chosen record's name. |
clear-button | The button that clears the chosen record. |
chips | The row of chosen records and the input, in multi-select mode. |
chip | One chosen record. |
chip-label | A chosen record's name. |
chip-remove | The button that removes one chosen record. |
input | The native <input>. |
clear-all | The Clear all button. |
panel | The surface that opens: a dropdown's list, a popover, a tooltip, a dialog, a loading overlay's box. |
listbox | The list of options. |
group | A group of options. |
group-label | A group's heading. |
option | One option. |
option-label | An option's label. |
option-description | An option's second line. |
status | A status line (a page count, "Searching…"). |
empty | What shows when there is nothing to list. |
help-text | The help text under the field. |
error-text | The error under the field. In the page even while empty, so style it only for a field that is invalid. |
States are extra names on a part while the state is on. Name both to style one state, e.g.fandry-checkbox::part(control checked).
| State | Description |
|---|---|
disabled | A control, option or item that cannot be used. |
invalid | A field that has error text. |
selected | The chosen option, a selected table row, or the selected tab. |
active | The option the keyboard or pointer is on. |
<!-- template -->
<div class="brand narrow">
<fandry-lookup
label="Account"
placeholder="Search accounts"
debounce-ms="300"
results={results}
loading={loading}
record={account}
onsearch={handleSearch}
onchange={handleChange}
></fandry-lookup>
</div>
/* css */
/* Scoped to this demo. Set the same --fd-* tokens on :root to theme
every fandry-* component on the site. */
.brand {
--fd-radius-md: 999px;
--fd-border-focus: 160 84% 36%;
--fd-ring-color: 160 84% 36%;
--fd-bg-muted: 160 40% 94%;
}
fandry-lookup::part(control) {
padding-inline: 0.75rem;
background: hsl(160 40% 98%);
}
fandry-lookup::part(panel) {
border-radius: 1rem;
box-shadow: 0 12px 32px hsl(160 40% 20% / 0.18);
}
fandry-lookup::part(option-description) {
font-style: italic;
}
Code
Link to section Code<!-- template -->
<fandry-lookup
label="Account"
placeholder="Search accounts"
results={results}
loading={loading}
record={account}
onsearch={handleSearch}
onchange={handleChange}
></fandry-lookup>
// component
async handleSearch(event) {
this.loading = true;
// Apex, GraphQL, UI API -- whatever you already use:
this.results = await findAccounts(event.detail.query);
this.loading = false;
}
handleChange(event) {
this.account = event.detail.record; // null when cleared
}
// Ignore a slow earlier answer landing after a newer one: keep a request
// counter and only apply the result of the latest.
Example: Multiple records
Link to section Example: Multiple recordsCode
<!-- template: bind `records` instead of `record` -->
<fandry-lookup
label="Accounts"
multiple
results={results}
loading={loading}
records={accounts}
onsearch={handleSearch}
onchange={handleChange}
></fandry-lookup>
// component
handleChange(event) {
this.accounts = event.detail.records; // also event.detail.values (the ids)
}
// The list stays open after each pick and fires `search` again with an
// empty query, so refresh it (recent records, minus what's now chosen).
// Records already chosen are hidden from `results` automatically.
Example: Existing value (single and multiple)
Link to section Example: Existing value (single and multiple)Code
<!-- template: bind `value`. Bind `record`/`records` too, to supply names. -->
<fandry-lookup
label="Account"
value={accountId}
record={account}
onresolve={handleResolve}
onchange={handleChange}
></fandry-lookup>
<fandry-lookup
label="Accounts"
multiple
value={accountIds}
records={accounts}
onresolve={handleResolveMany}
onchange={handleChangeMany}
></fandry-lookup>
// component
accountId = '001C'; // straight off a saved record
accountIds = ['001A', '001D'];
// The lookup can't name an id it has no record for. It shows the id, and asks:
async handleResolve(event) {
const [account] = await getAccounts(event.detail.values);
this.account = account;
}
async handleResolveMany(event) {
const found = await getAccounts(event.detail.values);
this.accounts = [...this.accounts, ...found];
}
// Only the ids it asked about are in event.detail.values. Records for ids
// that aren't in `value` are ignored: value decides what's selected.
handleChange(event) {
this.accountId = event.detail.value;
}
handleChangeMany(event) {
this.accountIds = event.detail.values;
this.accounts = event.detail.records;
}