Skip to content

Combobox async options#831

Open
NoelDeMartin wants to merge 8 commits into
SolidOS:stagingfrom
NoelDeMartin:improvements
Open

Combobox async options#831
NoelDeMartin wants to merge 8 commits into
SolidOS:stagingfrom
NoelDeMartin:improvements

Conversation

@NoelDeMartin

@NoelDeMartin NoelDeMartin commented Jul 7, 2026

Copy link
Copy Markdown
Member

This PR improves the Combobox in a couple of ways, and it implements new features to support loading options asynchronously. I'm not 100% convinced about the naming, though, so suggestions are welcome.

It also improves the Select component to be backwards compatible with the previous API.

@NoelDeMartin NoelDeMartin force-pushed the improvements branch 2 times, most recently from 36b52a1 to f234b5d Compare July 7, 2026 14:58
NoelDeMartin and others added 4 commits July 8, 2026 10:28
Prompt: I just implemented async options management, and I realized we have a problem with options where the value and label is different. In the options list, they display properly, but when you select an option, what ends up in the input is the value, not the label; which is confusing from a UX perspective. Can we fix that?

AI work summary:

- Introduced a new state variable `displayValue` to manage the displayed value in the input.
- Updated the `onValueChanged` handler to set `displayValue` based on user input.
- Modified `selectOption` method to accept a `ComboboxOptionData` object and update `displayValue` accordingly.
- Ensured `displayValue` is initialized with the current value when the component updates.

Co-Authored-By: Cursor <cursoragent@cursor.com>
This will reduce unnecessary network requests
The previous implementation was a bit clunky with the autocomplete and default options list after opening with async options. It also split the concern for watching value changes in two parts (onValueChanged vs willUpdate). This implementation should be more reliable and it also adds some new functionality like fallback options, a selectedOption getter, the "change" event, etc.
- Add `options` getter and setter
- Add default option
- Implement `change` event
@NoelDeMartin NoelDeMartin marked this pull request as ready for review July 8, 2026 15:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the solid-ui-combobox to support asynchronously loaded options (via URL templates or a JS provider) and updates the solid-ui-select API to support providing options programmatically while maintaining backward compatibility with DOM-provided <solid-ui-*-option> children.

Changes:

  • Add a small debounce() utility and use it to debounce async combobox filtering.
  • Add async options support to Combobox (provider + URL-based fetching) and update rendering to support non-selectable / templated options.
  • Update Select to accept an options array and dispatch change details, while still supporting DOM child options.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/lib/timing.ts Adds debounce() helper used for async combobox filtering.
src/lib/components/traits/InputTrait.ts Broadens value handling to unknown and stringifies form values.
src/components/select/Select.ts Adds programmatic options API and change-event detail; keeps DOM fallback.
src/components/select/index.ts Re-exports Select module types/symbols.
src/components/combobox/index.ts Re-exports Combobox module types/symbols.
src/components/combobox/Combobox.ts Implements async options support, debounced filtering, and richer option rendering.
src/components/combobox/Combobox.styles.css Styles loading/error/info rows for async options.
src/components/combobox/Combobox.stories.ts Adds Storybook examples for async options (JS provider + URL).
package.json Bumps version and adds @lit/task dependency.
package-lock.json Locks @lit/task dependency and bumps version.
Comments suppressed due to low confidence (1)

src/components/select/Select.ts:93

  • <select>.value is always a string, but option values are typed as unknown. Without normalizing, ?selected can be wrong (e.g. option value 1 vs current value '1'). Normalizing to strings here aligns DOM selection with the component’s string value.
              html`<option
                .value=${option.value}
                ?selected=${option.value === this.value}
              >

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/timing.ts
Comment on lines +10 to +17
let timeout: unknown

const debouncedCallback: DebouncedFunction<Args> = (...args: Args) => {
debouncedCallback.cancel()
timeout = setTimeout(() => callback(...args), delay)
}

debouncedCallback.cancel = () => clearTimeout(timeout as number)

firstUpdated () {
this.config.getInternals().setFormValue(this.target.value)
this.config.getInternals().setFormValue(String(this.target.value))
Comment on lines +64 to 68
setValue (value: unknown) {
this.target.value = value

this.config.getInternals().setFormValue(this.target.value)
this.config.getInternals().setFormValue(String(this.target.value))
this.target.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
Comment on lines +49 to +52
return Array.from(options).map((option) => ({
value: option.value,
label: option.textContent,
}))
Comment on lines +103 to +107
private onChange () {
const value = this.inputElement?.value
const option = this.options.find((option) => option.value === value)

return Array.from(options).map((option) => ({
value: option.value,
label: option.textContent,
}))
this.inputTrait.setValue(value)
Comment on lines +326 to +342
const response = await fetch(
this.asyncOptionsUrl.replace('%search%', filter)
)
const data = await response.json()
const results = Array.from(
this.asyncOptionsResultsField
? data[this.asyncOptionsResultsField]
: data
)

if (results.length === 0) {
throw new AsyncOptionsInfo('No results found')
}

const labelField = this.asyncOptionsLabelField ?? 'label'
const valueField = this.asyncOptionsValueField ?? 'value'

Comment on lines +73 to +75
const defaultOption = this.options.some(option => !option.value)
? nothing
: html`<option disabled value="" ?selected=${!this.value}>Select an option</option>`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix combo boxes

2 participants