Skip to content

Commit cddd702

Browse files
committed
SolidOS/profile-pane#422 Implement async combobox options provider
1 parent d0c1766 commit cddd702

3 files changed

Lines changed: 102 additions & 31 deletions

File tree

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { html } from 'lit'
22
import { defineStoryRender } from '@/storybook'
3+
import { defineAsyncComboboxOptionsProvider } from './Combobox'
34

45
import '@/components/combobox-option'
56

@@ -10,45 +11,96 @@ const meta = {
1011
args: {
1112
label: 'What is the best food?',
1213
options: 'Pizza, Ramen, Tacos',
13-
asyncOptions: false
14+
asyncJSOptions: false,
15+
asyncHtmlOptions: false,
1416
},
1517
argTypes: {
1618
label: { control: 'text' },
1719
options: { control: 'text' },
18-
asyncOptions: { control: 'boolean' },
20+
asyncJSOptions: { control: 'boolean' },
21+
asyncHtmlOptions: { control: 'boolean' },
1922
},
2023
} as const
2124

22-
const render = defineStoryRender<typeof meta.argTypes>(({ label, options, asyncOptions }) => {
23-
const parsedOptions = options.split(',').map(option => option.trim())
25+
const pokemonProvider = defineAsyncComboboxOptionsProvider(async (query) => {
26+
const response = await fetch('https://beta.pokeapi.co/graphql/v1beta', {
27+
method: 'POST',
28+
headers: { 'Content-Type': 'application/json' },
29+
body: JSON.stringify({
30+
query: `
31+
query searchPokemon($search: String!) {
32+
pokemon_v2_pokemon(where: {name: {_ilike: $search}}, limit: 20) {
33+
id
34+
name
35+
}
36+
}
37+
`,
38+
variables: { search: `%${query}%` },
39+
}),
40+
})
41+
42+
const { data } = await response.json()
43+
44+
return data.pokemon_v2_pokemon.map(pokemon => ({
45+
value: pokemon.id.toString(),
46+
label: pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1),
47+
}))
48+
})
49+
50+
const render = defineStoryRender<typeof meta.argTypes>(
51+
({ label, options, asyncJSOptions, asyncHtmlOptions }) => {
52+
if (asyncJSOptions) {
53+
return html`
54+
<solid-ui-combobox
55+
label="${label}"
56+
.asyncOptionsProvider=${pokemonProvider}
57+
></solid-ui-combobox>
58+
`
59+
}
60+
61+
if (asyncHtmlOptions) {
62+
return html`
63+
<solid-ui-combobox
64+
label="${label}"
65+
async-options-url="https://api.disneyapi.dev/character?name=%search%"
66+
async-options-results-field="data"
67+
async-options-label-field="name"
68+
async-options-value-field="_id"
69+
></solid-ui-combobox>
70+
`
71+
}
72+
73+
const parsedOptions = options.split(',').map((option) => option.trim())
2474

25-
if (asyncOptions) {
2675
return html`
27-
<solid-ui-combobox
28-
label="${label}"
29-
async-options-url="https://api.disneyapi.dev/character?name=%search%"
30-
async-options-results-field="data"
31-
async-options-label-field="name"
32-
async-options-value-field="_id"
33-
></solid-ui-combobox>
76+
<solid-ui-combobox label="${label}">
77+
${parsedOptions.map(
78+
(option) =>
79+
html`<solid-ui-combobox-option value="${option}"
80+
>${option}</solid-ui-combobox-option
81+
>`
82+
)}
83+
</solid-ui-combobox>
3484
`
3585
}
36-
37-
return html`
38-
<solid-ui-combobox label="${label}">
39-
${parsedOptions.map(option => html`<solid-ui-combobox-option value="${option}">${option}</solid-ui-combobox-option>`)}
40-
</solid-ui-combobox>
41-
`
42-
})
86+
)
4387

4488
export default meta
4589

4690
export const Primary = { render }
4791

48-
export const Async = {
92+
export const AsyncWithJS = {
93+
args: {
94+
label: 'Who is the best Pokemon?',
95+
asyncJSOptions: true,
96+
},
97+
render,
98+
}
99+
100+
export const AsyncWithHtml = {
49101
args: {
50102
label: 'Who is the best Disney character?',
51-
asyncOptions: true
103+
asyncHtmlOptions: true,
52104
},
53-
render
105+
render,
54106
}

src/components/combobox/Combobox.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ import styles from './Combobox.styles.css'
1414

1515
class AsyncOptionsInfo extends Error {}
1616

17-
type ComboboxOptionData = {
17+
export type ComboboxOptionData = {
1818
value: string;
1919
label: string | TemplateResult;
20-
selectable: boolean;
20+
selectable?: boolean;
21+
}
22+
23+
export type AsyncComboboxOptionsProvider = (query: string) => Promise<ComboboxOptionData[]>
24+
25+
export function defineAsyncComboboxOptionsProvider<T extends AsyncComboboxOptionsProvider> (provider: T): T {
26+
return provider
2127
}
2228

2329
@customElement('solid-ui-combobox')
@@ -52,6 +58,9 @@ export default class Combobox extends WebComponent {
5258
@property({ type: String, attribute: 'async-options-value-field' })
5359
accessor asyncOptionsValueField = ''
5460

61+
@property({ type: Function })
62+
accessor asyncOptionsProvider: AsyncComboboxOptionsProvider | null = null
63+
5564
@query('input')
5665
private accessor inputElement: HTMLInputElement | null = null
5766

@@ -123,7 +132,7 @@ export default class Combobox extends WebComponent {
123132
this.displayValue = this.value
124133
}
125134

126-
if (changedProperties.has('asyncOptionsUrl')) {
135+
if (changedProperties.has('asyncOptionsUrl') || changedProperties.has('asyncOptionsProvider')) {
127136
this.updateAsyncOptionsTask()
128137
}
129138
}
@@ -183,7 +192,7 @@ export default class Combobox extends WebComponent {
183192
@mousedown=${this.onListboxMouseDown}
184193
>
185194
${options.map((option, index) => {
186-
return option.selectable
195+
return option.selectable !== false
187196
? html`<div
188197
id=${this.getOptionId(index)}
189198
role="option"
@@ -243,7 +252,6 @@ export default class Combobox extends WebComponent {
243252
return Array.from(options).map((option) => ({
244253
value: option.value,
245254
label: option.textContent ?? '',
246-
selectable: true,
247255
}))
248256
}
249257

@@ -278,7 +286,7 @@ export default class Combobox extends WebComponent {
278286
}
279287

280288
private updateAsyncOptionsTask () {
281-
if (!this.asyncOptionsUrl) {
289+
if (!this.asyncOptionsUrl && !this.asyncOptionsProvider) {
282290
this.asyncOptionsTask = undefined
283291

284292
return
@@ -287,6 +295,16 @@ export default class Combobox extends WebComponent {
287295
this.asyncOptionsTask ??= new Task(
288296
this,
289297
async ([filter]) => {
298+
if (this.asyncOptionsProvider) {
299+
const results = await this.asyncOptionsProvider(filter)
300+
301+
if (results.length === 0) {
302+
throw new AsyncOptionsInfo('No results found')
303+
}
304+
305+
return results
306+
}
307+
290308
const response = await fetch(
291309
this.asyncOptionsUrl.replace('%search%', filter)
292310
)
@@ -307,7 +325,6 @@ export default class Combobox extends WebComponent {
307325
return results.map((result) => ({
308326
label: String(Object(result)[labelField]),
309327
value: String(Object(result)[valueField]),
310-
selectable: true,
311328
}))
312329
},
313330
() => [this.debouncedFilter]

src/components/combobox/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import Combobox from './Combobox'
1+
import Combobox, { defineAsyncComboboxOptionsProvider } from './Combobox'
2+
import type { AsyncComboboxOptionsProvider, ComboboxOptionData } from './Combobox'
23

3-
export { Combobox }
4+
export { Combobox, defineAsyncComboboxOptionsProvider }
5+
export type { AsyncComboboxOptionsProvider, ComboboxOptionData }
46
export default Combobox

0 commit comments

Comments
 (0)