Home > database >  Custom autocomplete input clearing selected option. `update:modelValue` issue
Custom autocomplete input clearing selected option. `update:modelValue` issue

Time:03-03

I'm tyring to create a autocomplete input with deboand for some reason I am not able to get it to work properly. I am able to select a suggestion from the drop down and show it briefly on the screen, but then it resets alongside the input itself.

I'm pretty sure the error comes from the selectSuggestion function's last line to clear the valueInner, but clearing the input once the value is selected is what I want. I don't want, however, the selected value to clear after that has happened.

It could be a combination of the selectSuggestion function and the watch on valueInner, but I also need that watch to make the debounce work.

Any ideas on how I can fix this?

CodeSandbox Link: https://codesandbox.io/s/stoic-forest-7w19oj?file=/src/components/Autocomplete.vue

App.vue

<template>
  <Autocomplete v-model="text" :suggestions="['something','hairstyle','cheese']"/>
  <p>Selected Suggestion:{{text}}</p>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import Autocomplete from '../src/components/Autocomplete'

export default defineComponent({
  name: "App",
  setup(){
    const text = ref('')
    return { text }
  },
  components: {
  Autocomplete,
  Autocomplete2,
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Autocomplete

<template>
    <input
      ref="inputRef"
      v-model="valueInner"
      :disabled="disabled || undefined"
      :placeholder="placeholder"
      :readonly="readonly || undefined"
      :required="required || undefined"
      :autofocus="autofocus"
      :autocomplete="autocomplete"
      :suggestions="suggestions"
    />
    <ul v-if="filterSuggestions.length">
      <li>
        Showing {{ filterSuggestions.length }} of {{ suggestions.length }} results
      </li>
      <li
        v-for="suggestion in filterSuggestions"
        :key="suggestion"
        @click="selectSuggestion(suggestion)"
      >
        {{ suggestion}}
      </li>
    </ul>
</template>

<script lang="ts">
import {
  defineComponent,
  ref,
  onMounted,
  nextTick,
  watch,
  PropType,
  computed,
} from 'vue'
export default defineComponent({
  name: 'Autocomplete',
  inheritAttrs: false,
  props: {
    label: {
      type: String,
    },
    disabled: { type: Boolean, default: false },
    placeholder: { type: String },
    readonly: { type: Boolean },
    required: { type: Boolean },
    modelValue: { type: [String, Number], default: '' },
    autocomplete: { type: Boolean, default: false },
    suggestions: {
      type: Array as PropType<string[]>,
      default: ['Hamburger', 'Fries', 'Peanuts'],
    },
    debounce: { type: Number, default: undefined },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    let debounceInner = 0
    if (typeof props.debounce === 'number') {
      debounceInner = props.debounce
    } debounceInner = 1000
    const inputRef = ref<HTMLElement | null>(null)
    const isFocused = ref(false)
    watch(isFocused, (oldVal, newVal) => {
      console.log(`isFocused → `, isFocused)
    })
    let timeout: any = null

    const valueInner = ref<any>(props.modelValue)
    console.log(valueInner.value)
    watch(valueInner, (newVal, oldVal) => {
      const debounceMs = debounceInner
      if (debounceMs > 0) {
        clearTimeout(timeout)
        timeout = setTimeout(() => emitInput(newVal), debounceMs)
      } else {
        emitInput(newVal)
      }
    })
    function emitInput(newVal: any) {
      let payload = newVal
      emit('update:modelValue', payload)
    }
    let selectedSuggestion = ref('')
    const suggestions = ['United States', 'Japan', 'Italy', 'Australia', 'Germany', 'Poland', 'Ukraine']
    const filterSuggestions = computed(() => {
      if (valueInner.value === '') {
          return []
          }
      let matches = 0
      return suggestions.filter((suggestion) =>{
        if (
          suggestion.toLowerCase().includes(valueInner.value.toLowerCase())
          && matches < 10
        ) {
          matches  
          return suggestion
        }
       })
    })
    const selectSuggestion = (suggestion) => {
      selectedSuggestion.value = suggestion
      emitInput(suggestion)
      valueInner.value = ''
    }
    function emitInput(newVal: any) {
      let payload = newVal
      emit('update:modelValue', payload)
    }
    watch(valueInner, (newVal, oldVal) => {
      const debounceMs = debounceInner
      if (debounceMs > 0) {
        clearTimeout(timeout)
        timeout = setTimeout(() => emitInput(newVal), debounceMs)
      } else {
        emitInput(newVal)
      }
    })
    return {
      inputRef,
      valueInner,
      suggestions,
      filterSuggestions,
      selectSuggestion,
      selectedSuggestion,
    }
  },
})
</script>

<style lang="sass" scoped>
li
  list-style: none
</style>

CodePudding user response:

The error is indeed from clearing valueInner.value, which triggers the watch on it, which emits the blank value as the update:modelValue event data, which overwrites the model value.

A quick fix is to use null instead of an empty string (to distinguish between an internal clear versus user input) when resetting valueInner.value:

const selectSuggestion = (suggestion) => {
  selectedSuggestion.value = suggestion
  emitInput(suggestion)
  valueInner.value = null            
  • Related