From 5f8fe8559f2eb12d3916387d5acf65a391bfa0eb Mon Sep 17 00:00:00 2001 From: Marc-Olivier Castagnetti <6144489+mcastagnetti@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:58:16 +0200 Subject: [PATCH] feat(SelectMenu): handle async search (#426) Co-authored-by: Benjamin Canac --- .../examples/SelectMenuExampleAsyncSearch.vue | 19 +++++++++++ docs/content/3.forms/4.select-menu.md | 34 +++++++++++++++++++ src/runtime/components/forms/SelectMenu.vue | 33 ++++++++++++------ 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 docs/components/content/examples/SelectMenuExampleAsyncSearch.vue diff --git a/docs/components/content/examples/SelectMenuExampleAsyncSearch.vue b/docs/components/content/examples/SelectMenuExampleAsyncSearch.vue new file mode 100644 index 00000000..77fa206f --- /dev/null +++ b/docs/components/content/examples/SelectMenuExampleAsyncSearch.vue @@ -0,0 +1,19 @@ + + + diff --git a/docs/content/3.forms/4.select-menu.md b/docs/content/3.forms/4.select-menu.md index 26116650..ed48128a 100644 --- a/docs/content/3.forms/4.select-menu.md +++ b/docs/content/3.forms/4.select-menu.md @@ -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 + + + +``` +:: + ## Slots ### `label` diff --git a/src/runtime/components/forms/SelectMenu.vue b/src/runtime/components/forms/SelectMenu.vue index 5bdf6b61..6398aaba 100644 --- a/src/runtime/components/forms/SelectMenu.vue +++ b/src/runtime/components/forms/SelectMenu.vue @@ -129,6 +129,7 @@ import { ListboxOptions as HListboxOptions, ListboxOption as HListboxOption } from '@headlessui/vue' +import { computedAsync, useDebounceFn } from '@vueuse/core' import { defu } from 'defu' import UIcon from '../elements/Icon.vue' import UAvatar from '../elements/Avatar.vue' @@ -219,13 +220,17 @@ export default defineComponent({ default: false }, searchable: { - type: Boolean, + type: [Boolean, Function] as PropType Promise | any[])>, default: false }, searchablePlaceholder: { type: String, default: 'Search...' }, + debounce: { + type: Number, + default: 200 + }, creatable: { type: Boolean, default: false @@ -373,15 +378,23 @@ export default defineComponent({ ) }) - const filteredOptions = computed(() => - query.value === '' - ? props.options - : (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 debouncedSearch = typeof props.searchable === 'function' ? useDebounceFn(props.searchable, props.debounce) : undefined + + const filteredOptions = computedAsync(async () => { + if (props.searchable && debouncedSearch) { + return await debouncedSearch(query.value) + } + + 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(() => { return query.value === '' ? null : { [props.optionAttribute]: query.value }