<template> <div class="autocomplete"> <div class="input"> <input type="text" :id="id" :name="name" :placeholder="placeholder" v-model="newValue" @focus="onFocus" @blur="onBlur" ref="input" /> </div> <div class="values" v-if="showValues" @click.stop> <ul> <li v-for="value in filteredValues" :key="value.value" @click.stop="onItemClick(value)"> {{ value.label }} </li> </ul> </div> </div> </template> <script lang="ts"> import AutocompleteValue from '../models/AutocompleteValue'; export default { emits: ['input'], props: { id: { type: String, default: '', }, name: { type: String, default: '', }, value: { type: String, default: '', }, values: { type: Array as () => AutocompleteValue[], default: () => [], }, allowOnlyValues: { type: Boolean, default: false, }, placeholder: { type: String, default: 'Search...', }, }, data() { return { newValue: '' + this.value, showValues: false, }; }, computed: { filteredValues() { if (!this.newValue?.length) { return this.values; } let matches = this.values.filter((value: AutocompleteValue) => value.value.toLowerCase() === this.newValue.toLowerCase() ) as AutocompleteValue[]; if (!matches.length) { matches = this.values.filter((value: AutocompleteValue) => value.label.toLowerCase().includes(this.newValue.toLowerCase()) ) as AutocompleteValue[]; } return matches; }, indexedValues() { return Object.fromEntries( this.values.map((value: AutocompleteValue) => [ value.value, value, ]) ); }, }, methods: { onFocus() { this.showValues = true; }, onBlur() { setTimeout(() => { this.showValues = false; this.emitInput(); }, 500); }, onItemClick(value: AutocompleteValue) { this.newValue = value.value; this.showValues = false; this.emitInput(); }, emitInput() { this.$emit( 'input', this.allowOnlyValues ? this.indexedValues[this.newValue]?.value : this.newValue ) } }, watch: { value(newValue: string) { this.newValue = newValue; }, newValue() { this.emitInput(); }, }, } </script> <style lang="scss" scoped> .autocomplete { width: 100%; position: relative; .input { input { width: 100%; } } .values { position: absolute; top: 100%; left: 0; right: 0; max-height: 20em; overflow: auto; background-color: var(--color-background); z-index: 10; box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.5); ul { list-style-type: none; margin: 0; padding: 0; li { padding: 0.8em; cursor: pointer; &:hover { background-color: var(--color-accent-bg); } } } } } </style>