Skip to content
Snippets Groups Projects
Commit 361cf868 authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt :koala:
Browse files

feat: implement pluggable search in ComboBoxSimple component

Sometimes we don’t have a static list of choices but an async source for
them (like an API endpoint). This is now supported out of the box
through the searchProvider prop.
parent fb7c4aad
No related branches found
No related tags found
No related merge requests found
...@@ -35,13 +35,22 @@ ...@@ -35,13 +35,22 @@
</ComboBox> </ComboBox>
</template> </template>
<script setup lang="ts" generic="T extends { id: ID, name: string }"> <script lang="ts">
import type { ComboBoxProps } from '@/components/ComboBox.vue'
export type ComboBoxSimpleProps<T> = Omit<ComboBoxProps<T>, 'choices'> & {
choices?: T[]
searchProvider?: (query: string, signal: AbortSignal) => Promise<T[]>
}
</script>
<script setup lang="ts" generic="T extends { id: ID, name?: string }">
import { ID } from '@/api' import { ID } from '@/api'
import Tag from './generic/Tag.vue' import Tag from './generic/Tag.vue'
import ComboBox from './ComboBox.vue' import ComboBox from './ComboBox.vue'
import type { ComboBoxProps } from '@/components/ComboBox.vue' import { computed, ref, toValue } from 'vue'
import { computed, ref } from 'vue' import { computedDebounced, matchesSearch } from '@/util'
import { matchesSearch } from '@/util' import { computedAsync } from '@vueuse/core'
defineOptions({ defineOptions({
compatConfig: { MODE: 3 }, compatConfig: { MODE: 3 },
...@@ -63,9 +72,10 @@ defineSlots<{ ...@@ -63,9 +72,10 @@ defineSlots<{
}>() }>()
const modelValue = defineModel<null | T | T[]>({ required: true }) const modelValue = defineModel<null | T | T[]>({ required: true })
const props = defineProps<ComboBoxProps<T>>() const props = defineProps<ComboBoxSimpleProps<T>>()
const searchQuery = ref('') const searchQuery = ref('')
const debouncedSearchQuery = computedDebounced(searchQuery, (t) => (t.trim() ? 0.3 : 0))
const selectedIds = computed(() => { const selectedIds = computed(() => {
if (Array.isArray(modelValue.value)) { if (Array.isArray(modelValue.value)) {
return modelValue.value.map((item) => item.id) return modelValue.value.map((item) => item.id)
...@@ -76,9 +86,29 @@ const selectedIds = computed(() => { ...@@ -76,9 +86,29 @@ const selectedIds = computed(() => {
} }
}) })
let abortController: AbortController | null = null
const searchedChoices = computedAsync(
async () => {
if (props.choices) {
const query = toValue(searchQuery)
return props.choices.filter(({ name }) => matchesSearch(name ?? '', query))
} else if (props.searchProvider) {
const query = toValue(debouncedSearchQuery)
if (abortController) abortController.abort()
abortController = new AbortController()
try {
return await props.searchProvider(query, abortController.signal)
} finally {
abortController = null
}
} else {
return []
}
},
props.choices ?? [],
{ lazy: false },
)
const filteredChoices = computed(() => const filteredChoices = computed(() =>
props.choices.filter( searchedChoices.value.filter((choice) => !selectedIds.value.includes(choice.id)),
({ id, name }) => !selectedIds.value.includes(id) && matchesSearch(name, searchQuery.value),
),
) )
</script> </script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment