skip to Main Content

I want to create a search component with result dropdown in vue js using composition api.
The component should follow the next requirements:

  1. 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;
  2. Clicking outside the input the dropdown should close
  3. Clocking on X button the input text should be removed
  4. 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:

  1. When i click on the input first appear result and only the loading.
  2. 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


  1. 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.

    Login or Signup to reply.
  2. 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.:

    <Search :loading="loading" @on-update="(v) => {loading = true; search(v)}" :items="elements"/>
    

    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.

    <script setup>
    const clickItem = (el) => {
      inputValue.value = el
      isOpen.value = false
    }
    const onBlur = (ev) => {
      if(ev.relatedTarget?.className === 'list-item') return
      isOpen.value = false
    }
    <script>
    <template>
      <div>
        <div><input @focus="onFocus" @blur="onBlur" @input="updateValue" v-model="inputValue"/>
        <button @click="inputValue = '';isOpen = false">clear</button>
        </div>
        <div>
          <ul v-show="!loading && items.length && isOpen">
          <li class="list-item" style="background-color: red" tabindex="-1" @click="clickItem(item.title)" v-for="item in items">{{ item.title}}</li>
        </ul>
        <span v-if="loading">Loading...</span>
        </div>
      </div>
    </template> 
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search