I want to create a search component with result dropdown in vue js using composition api.
The component should follow the next requirements:
- Clicking on the input a request should occur, loading component should appear, the API needs to return all results and renders in the result dropdown;
- Clicking outside the input the dropdown should close
- Clocking on X button the input text should be removed
- Clicking on a result the value should appear in the input and the dropdown should disappear
Here is my vue js code:
Search.vue
<script setup>
import { ref, toRefs } from 'vue';
const props = defineProps(['items', 'loading'])
const {items, loading} = toRefs(props)
const inputValue = ref('')
const isOpen = ref(false)
const emit = defineEmits(['on-update'])
const updateValue = (event) => {
isOpen.value = true
emit('on-update', event.target.value)
};
const clickItem = (el) => {
inputValue.value = el
}
const onFocus = () => {
isOpen.value = true;
emit('on-update', '')
}
</script>
<template>
<div>
<div><input @focus="onFocus" @blur="isOpen = false" @input="updateValue" v-model="inputValue"/>
<button @click="inputValue = '';isOpen = false">clear</button>
</div>
<div>
<ul v-if="!loading && items.length && isOpen">
<li style="background-color: red" @click="clickItem(item.title)" v-for="item in items">{{ item.title}}</li>
</ul>
<span v-if="loading">Loading...</span>
</div>
</div>
</template>
and here is my parent component:
<script setup>
import { ref } from 'vue'
import Search from './Search.vue'
const elements = ref([])
const loading = ref(false)
function debounce (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
const search = debounce( (searchValue = '') => {
loading.value = true
fetch(`https://dummyjson.com/products/search?q=${searchValue}`)
.then(res => {
return res.json()
})
.then((data) => {
elements.value = data.products
})
.finally(() => loading.value = false);
}, 600)
</script>
<template>
<Search :loading="loading" @on-update="(v) => search(v)" :items="elements"/>
</template>
NOTE: demo link
Issues:
- When i click on the input first appear result and only the loading.
- Clicking on the result item, the dropdown is closed but the value does not appear in the input value.
Question: How to fix all these issues?
2
Answers
It is because the
@blur
fires earlier than the@click
and therefore the click is ignored because the blur removes anything ‘inside’ it. One way to fix this is changing the@click
to a@mousedown
event.The first problem is caused by the debouncing of
loading.value = true
switch. The solution is to switch the loading flag outside the debounced function, for ex.:The second problem, like @FerryKranenburg suggested, is caused by the blur event unmounting the list before the click event propagates to list items, since blur event takes precedence over click event. Other than using
@mousedown
event instead of@click
, you can check the related target of the blur event and prevent hiding the list items in case the target is the list item. Credit to this answer.