Skip to content

Instantly share code, notes, and snippets.

@vmitchell85
Created January 21, 2022 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vmitchell85/23dc7977ba7239747cbd73f199ba070b to your computer and use it in GitHub Desktop.
Save vmitchell85/23dc7977ba7239747cbd73f199ba070b to your computer and use it in GitHub Desktop.
First pass at a ComboBox using TailwindCSS
<template>
<div>
<label v-if="label" :for="name" class="block text-sm font-medium text-gray-700">
{{ label }}
</label>
<div class="relative mt-1">
<input v-show="is_editing" ref="input" v-model="query" :name="name" class="block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @keydown="inputKeyDown">
<div v-show="!is_editing" @click="startEditing" class="block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
{{ selectedOption ? selectedOption.text : blankText }}
</div>
<div v-show="is_editing" class="absolute bg-white shadow-lg rounded-md w-full mt-1 grid grid-cols-1 divide-y divide-gray-300 text-gray-800 overflow-auto max-h-[12rem]">
<div v-for="(option, idx) in filteredOptions" :key="idx" :id="'option-' + idx" class="px-4 py-2 cursor-pointer hover:bg-gray-200" :class="idx == activeOptionIndex ? 'bg-indigo-100' : ''" @click="selectOption(option)">
<span>{{ option.text }}</span>
</div>
</div>
</div>
<div class="mt-1 text-xs text-red-500">
<span v-if="hasError">{{ errors[name][0] }}</span>
<span v-else>&nbsp;</span>
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: {
type: [String, Number],
default() {
return '';
}
},
options: Array,
label: String,
name: {
type: String,
default() {
return 'combo-box-' + (Math.random() + 1).toString(36).substring(7);
}
},
errors: Object,
noBlankOption: Boolean,
blankText: {
type: String,
default: 'Select...',
}
},
data() {
return {
is_editing: false,
query: '',
selection: '',
selectedOption: null,
activeOptionIndex: 0,
};
},
computed: {
filteredOptions() {
var filtered = this.options.filter(option => option.text.toLowerCase().includes(this.query.toLowerCase()));
if (!this.noBlankOption) {
return [{text: 'Select...', value: ''}].concat(filtered);
}
return filtered;
},
hasError() {
return this.errors && Object.prototype.hasOwnProperty.call(this.errors, this.name);
}
},
created() {
window.addEventListener('click', (e) => {
if (!this.$el.contains(e.target)){
this.is_editing = false;
}
})
},
methods: {
selectOption(option) {
this.selectedOption = option;
this.is_editing = false;
this.query = '';
},
startEditing() {
this.is_editing = true;
this.activeOption = this.filteredOptions[0];
this.$nextTick(() => {
this.$refs.input.focus();
});
},
inputKeyDown(e) {
switch(e.key) {
case 'Enter':
this.selectOption(this.filteredOptions[this.activeOptionIndex]);
break;
case 'ArrowDown':
if (this.activeOptionIndex < this.filteredOptions.length - 1) {
this.activeOptionIndex++;
} else {
this.activeOptionIndex = 0;
}
break;
case 'ArrowUp':
if (this.activeOptionIndex > 0) {
this.activeOptionIndex--;
} else {
this.activeOptionIndex = this.filteredOptions.length - 1;
}
break;
default:
if (e.key.length == 1) {
this.activeOptionIndex = 0;
}
break;
}
this.scrollToSelectedIndex();
},
scrollToSelectedIndex() {
const selectedOption = document.getElementById(`option-${this.activeOptionIndex}`);
if (selectedOption) {
selectedOption.scrollIntoView({behavior: 'smooth', block: "end"});
}
}
},
watch: {
modelValue: {
immediate: true,
handler(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
var option = this.options.find(option => option.value == newVal);
if (option) {
this.selectedOption = option;
} else {
this.selectedOption = this.options[0];
}
} else {
if (this.noBlankOption) {
this.selectedOption = this.options[0];
}
}
}
},
selectedOption: {
immediate: true,
handler(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
this.$emit('update:modelValue', newVal.value);
}
}
}
}
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment