自定义自动完成输入清除选择的选项。 `update:modelValue` 问题

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

我很想用 deboand 创建一个自动完成输入,但出于某种原因我无法让它正常工作。 我可以 select 从下拉菜单中选择一个建议并在屏幕上简短地显示它,但随后它会与输入本身一起重置。

我很确定错误来自 selectSuggestion 函数的最后一行以清除 valueInner,但是一旦值是 selected 就清除输入是我想要的.但是,我不希望 selected 值在此之后被清除。

它可能是 selectSuggestion 函数和 valueInner 上的 watch 的组合,但我还需要 watch 来使去抖动工作。

关于如何解决这个问题的任何想法?

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>

自动完成

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

错误确实来自清除 valueInner.value,这会触发其上的监视,它会发出空白值作为 update:modelValue 事件数据,从而覆盖模型值。

一个快速修复方法是在重置 valueInner.value 时使用 null 而不是空字符串(以区分内部清除与用户输入):

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

然后在手表中,如果新值是 null,则立即 return 因为我们不需要发出那个:

watch(valueInner, (newVal, oldVal) => {
  if (newVal === null) return
  ⋮
  /* handle new value */
})

此外,在 filterSuggestions 中,return 值为假值时的空数组(包括 null 或空字符串):

const filterSuggestions = computed(() => {
     
  if (!valueInner.value) {
    return []
  }
  ⋮
})

demo