(search) use debounce to prevent multiple requests while typing

This commit is contained in:
Valentyne Stigloher 2024-12-26 13:45:26 +01:00
parent 6a576b6c7a
commit 9318d64e44
7 changed files with 58 additions and 21 deletions

View File

@ -1,31 +1,34 @@
<script setup lang="ts">
const search = ref('');
import { useDebounce } from '@vueuse/core';
const isSearchBlank = computed(() => {
return search.value.trim().length === 0;
});
const query = ref('');
const debouncedQuery = useDebounce(query);
const searchAsyncData = useAsyncData('search', async () => {
if (isSearchBlank.value) {
if (debouncedQuery.value.trim().length === 0) {
return [];
}
return await $fetch('/api/search', {
query: {
text: search.value,
query: debouncedQuery.value,
},
});
}, {
watch: [search],
watch: [debouncedQuery],
});
const isLoading = computed(() => {
return query.value !== debouncedQuery.value || searchAsyncData.status.value === 'pending';
});
const hasNoResults = computed(() => {
return searchAsyncData.status.value !== 'pending' && searchAsyncData.data.value?.length === 0;
return !isLoading.value && searchAsyncData.data.value?.length === 0;
});
const dialogue = useTemplateRef('dialogue');
const open = () => {
search.value = '';
query.value = '';
searchAsyncData.clear();
dialogue.value?.showModal();
};
@ -59,11 +62,11 @@ defineExpose({ open, close });
<dialog ref="dialogue" class="container m-auto h-100 rounded border">
<div class="input-group mb-4">
<span class="input-group-text">
<Spinner v-if="searchAsyncData.status.value === 'pending'" size="1.25em" />
<Spinner v-if="isLoading" size="1.25em" />
<Icon v-else v="search" />
</span>
<input
v-model="search"
v-model="query"
type="search"
class="form-control border-primary"
:placeholder="$t('crud.filterLong')"
@ -72,7 +75,7 @@ defineExpose({ open, close });
<Icon v="times" />
</button>
</div>
<div v-if="isSearchBlank" class="alert alert-info">
<div v-if="query.trim().length === 0" class="alert alert-info">
<T>search.noInput</T>
</div>
<ul
@ -88,7 +91,7 @@ defineExpose({ open, close });
</li>
</ul>
<div v-else-if="hasNoResults" class="alert alert-info">
<T :params="{ search }">search.noResults</T>
<T :params="{ query }">search.noResults</T>
</div>
</dialog>
</template>

View File

@ -951,7 +951,7 @@ crud:
search:
header: 'Search'
noInput: 'Start searching by entering a search term'
noResults: 'No results for “%search%” found'
noResults: 'No results for “%query%” found'
footer:
license: >

View File

@ -931,7 +931,7 @@ crud:
search:
header: 'Suche'
noInput: 'Starte eine Suche durch Eingabe eines Suchbegriffs'
noResults: 'Keine Ergebnisse für „%search%“ gefunden'
noResults: 'Keine Ergebnisse für „%query%“ gefunden'
footer:
license: >

View File

@ -1153,7 +1153,7 @@ crud:
search:
header: 'Search'
noInput: 'Start searching by entering a search term'
noResults: 'No results for “%search%” found'
noResults: 'No results for “%query%” found'
footer:
license: >

View File

@ -28,6 +28,7 @@
"@sentry/vue": "^7.109.0",
"@vite-pwa/nuxt": "^0.10.1",
"@vuepic/vue-datepicker": "^8.8.1",
"@vueuse/core": "^12.2.0",
"abort-controller": "^3.0.0",
"autoprefixer": "^10.4.5",
"avris-columnist": "^0.3.4",

34
pnpm-lock.yaml generated
View File

@ -47,6 +47,9 @@ importers:
'@vuepic/vue-datepicker':
specifier: ^8.8.1
version: 8.8.1(vue@3.5.13(typescript@5.6.2))
'@vueuse/core':
specifier: ^12.2.0
version: 12.2.0(typescript@5.6.2)
abort-controller:
specifier: ^3.0.0
version: 3.0.0
@ -2892,6 +2895,9 @@ packages:
'@types/uuid@8.3.2':
resolution: {integrity: sha512-u40ViizKDmdl5FhOXn9WQbulpigYCaiD5hD4KqR3xyQww6l3+0ND+A9TeFla8tFpqvR+UAkJdYb/8jdaQG4/nw==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -3152,6 +3158,15 @@ packages:
peerDependencies:
vue: '>=3.2.0'
'@vueuse/core@12.2.0':
resolution: {integrity: sha512-jksyNu+5EGwggNkRWd6xX+8qBkYbmrwdFQMgCABsz+wq8bKF6w3soPFLB8vocFp3wFIzn0OYkSPM9JP+AFKwsg==}
'@vueuse/metadata@12.2.0':
resolution: {integrity: sha512-x6zynZtTh1l52m0y8d/EgzpshnMjg8cNZ2KWoncJ62Z5qPSGoc4FUunmMVrrRM/I/5542rTEY89CGftngZvrkQ==}
'@vueuse/shared@12.2.0':
resolution: {integrity: sha512-SRr4AZwv/giS+EmyA1ZIzn3/iALjjnWAGaBNmoDTMEob9JwQaevAocuaMDnPAvU7Z35Y5g3CFRusCWgp1gVJ3Q==}
'@webassemblyjs/ast@1.12.1':
resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==}
@ -11992,6 +12007,8 @@ snapshots:
'@types/uuid@8.3.2': {}
'@types/web-bluetooth@0.0.20': {}
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.16.5
@ -12442,6 +12459,23 @@ snapshots:
date-fns: 3.6.0
vue: 3.5.13(typescript@5.6.2)
'@vueuse/core@12.2.0(typescript@5.6.2)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 12.2.0
'@vueuse/shared': 12.2.0(typescript@5.6.2)
vue: 3.5.13(typescript@5.6.2)
transitivePeerDependencies:
- typescript
'@vueuse/metadata@12.2.0': {}
'@vueuse/shared@12.2.0(typescript@5.6.2)':
dependencies:
vue: 3.5.13(typescript@5.6.2)
transitivePeerDependencies:
- typescript
'@webassemblyjs/ast@1.12.1':
dependencies:
'@webassemblyjs/helper-numbers': 1.11.6

View File

@ -76,10 +76,10 @@ const loadIndices = async (
return new Map(indices.map((loadedKind) => [loadedKind.kind.kind, loadedKind]));
};
const searchIndices = (indices: Map<SearchDocument['kind'], LoadedSearchKind>, text: string): SearchResult[] => {
const searchIndices = (indices: Map<SearchDocument['kind'], LoadedSearchKind>, query: string): SearchResult[] => {
return [...indices.values()]
.flatMap(({ index }) => {
return index.search(text, { prefix: true, fuzzy: 1 });
return index.search(query, { prefix: true, fuzzy: 1 });
})
.toSorted((resultA, resultB) => {
return resultB.score - resultA.score;
@ -442,9 +442,8 @@ const SEARCH_LIMIT = 20;
export default defineEventHandler(async (event) => {
const indices = await loadIndices(kinds, global.config);
const query = getQuery(event);
const text = query.text as string;
return searchIndices(indices, text)
const query = getQuery(event).query as string;
return searchIndices(indices, query)
.slice(0, SEARCH_LIMIT)
.map((result) => transformResult(indices, result));
});