feat(SelectMenu): handle async search (#426)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Marc-Olivier Castagnetti
2023-07-18 15:58:16 +02:00
committed by GitHub
parent 46b444a3e0
commit 5f8fe8559f
3 changed files with 76 additions and 10 deletions

View File

@@ -0,0 +1,19 @@
<script setup>
const search = async (q) => {
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
const selected = ref([])
</script>
<template>
<USelectMenu
v-model="selected"
:searchable="search"
placeholder="Search for a user..."
multiple
by="id"
/>
</template>

View File

@@ -149,6 +149,40 @@ props:
--- ---
:: ::
### Async search
Pass a function to the `searchable` prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.
Use the `debounce` prop to adjust the delay of the function.
::component-example
#default
:select-menu-example-async-search{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const search = async (q) => {
const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
const selected = ref([])
</script>
<template>
<USelectMenu
v-model="selected"
:searchable="search"
placeholder="Search for a user..."
multiple
by="id"
/>
</template>
```
::
## Slots ## Slots
### `label` ### `label`

View File

@@ -129,6 +129,7 @@ import {
ListboxOptions as HListboxOptions, ListboxOptions as HListboxOptions,
ListboxOption as HListboxOption ListboxOption as HListboxOption
} from '@headlessui/vue' } from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
@@ -219,13 +220,17 @@ export default defineComponent({
default: false default: false
}, },
searchable: { searchable: {
type: Boolean, type: [Boolean, Function] as PropType<boolean | ((query: string) => Promise<any[]> | any[])>,
default: false default: false
}, },
searchablePlaceholder: { searchablePlaceholder: {
type: String, type: String,
default: 'Search...' default: 'Search...'
}, },
debounce: {
type: Number,
default: 200
},
creatable: { creatable: {
type: Boolean, type: Boolean,
default: false default: false
@@ -373,15 +378,23 @@ export default defineComponent({
) )
}) })
const filteredOptions = computed(() => const debouncedSearch = typeof props.searchable === 'function' ? useDebounceFn(props.searchable, props.debounce) : undefined
query.value === ''
? props.options const filteredOptions = computedAsync(async () => {
: (props.options as any[]).filter((option: any) => { if (props.searchable && debouncedSearch) {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => { return await debouncedSearch(query.value)
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1) }
})
}) if (query.value === '') {
) return props.options
}
return (props.options as any[]).filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
})
})
})
const queryOption = computed(() => { const queryOption = computed(() => {
return query.value === '' ? null : { [props.optionAttribute]: query.value } return query.value === '' ? null : { [props.optionAttribute]: query.value }