Creating a select with Alpine in Livewire
It took 3 days... three whole days. I'm really crazy. When I first made it, I was frustrated because I couldn't solve the overflow problem with CSS... (previous
<div x-data="
{
name: '선택',
value: @entangle('company_id'),
open: false,
data: {},
focusedOptionIndex: null,
options: {},
input: '',
previousInput: '',
fuse: null,
ing: false,
fuseInit: function () {
this.fuse = new Fuse(this.data, { includeMatches: true, threshold: 0.1, keys: ['name'] })
},
selectClick: function (data) {
this.open = !this.open
if(this.fuse === null) {
this.data = data
this.fuseInit()
}
},
openListbox: function (data) {
this.$nextTick(() => {
this.$refs.listbox.style.width = `${this.$refs.select.getBoundingClientRect().width}px`;
this.$refs.input.focus();
})
},
closeListbox: function () {
this.focusedOptionIndex = null
this.options = {}
this.input = ''
this.previousInput = ''
this.ing = false
},
focusNextOption: function () {
if (this.focusedOptionIndex + 1 >= Object.keys(this.options).length) return
this.focusedOptionIndex++
if(this.options[this.focusedOptionIndex] !== undefined) {
let pos = getRelativePos(this.$refs.list.children[this.focusedOptionIndex+1]);
if(pos.next !== undefined) {
this.$refs.list.scrollTop = pos.next;
}
}
},
focusPreviousOption: function () {
if (this.focusedOptionIndex <= 0) return
this.focusedOptionIndex--
if(this.options[this.focusedOptionIndex] !== undefined) {
let pos = getRelativePos(this.$refs.list.children[this.focusedOptionIndex+1]);
if(pos.next !== undefined) {
this.$refs.list.scrollTop = pos.next;
}
}
},
search: function () {
if(this.previousInput === this.input) return
this.options = this.fuse.search(this.input, { limit: 25 })
this.focusedOptionIndex = 0
this.$refs.list.scrollTop = 0
this.ing = false
this.previousInput = this.input
},
selectOption: function () {
if(this.input !== '' && this.options[this.focusedOptionIndex] !== undefined) {
let item = this.options[this.focusedOptionIndex].item
this.value = item.id
this.name = item.name
}
this.$refs.select.focus();
this.open = false
},
resize: function () {
if(this.open) this.$refs.listbox.style.width = `${this.$refs.select.getBoundingClientRect().width}px`;
},
closeEvent: function () {
this.open = false
this.fuse = null
this.name = '선택'
},
}"
x-init="
console.log(`select2-init`);
$watch('open', (value) => { if(value === true) { $nextTick(() => { openListbox() }) } else { closeListbox() } });
$watch('input', (value) => { ing = true });"
@close-event.window="closeEvent()"
x-on:resize.window.prevent="resize()"
>
<div class="relative">
<input type="text" class="shadow-sm focus:ring-gray-500 focus:border-gray-500 block w-full text-xs sm:text-sm border-gray-300 cursor-pointer"
placeholder="선택"
readonly
:class="{ 'rounded-t-md' : open , 'rounded-md' : !open }"
x-ref="select"
x-model="name"
x-on:click="selectClick($component('collection').company_list)"
x-on:keydown.enter.prevent="selectClick($component('collection').company_list)"
>
<div class="flex-col fixed bg-white"
x-ref="listbox"
x-show="open"
x-cloak>
<input type="text" class="shadow-sm focus:ring-gray-500 focus:border-gray-500 block w-full text-xs sm:text-sm border-gray-300"
placeholder="내용 입력"
x-model="input"
x-ref="input"
x-on:click.away="open = false"
x-on:input.debounce.250="search()"
x-on:keydown.enter.prevent="selectOption()"
x-on:keydown.arrow-up.prevent="focusPreviousOption()"
x-on:keydown.arrow-down.prevent="focusNextOption()"
>
<div class="max-h-60 overflow-auto border-2 border-t-0"
x-ref="list">
<template x-for="(key, index) in Object.keys(options)" :key="index">
<div class="p-2 bg-white border-gray-300 focus:border-gray-500 cursor-pointer"
x-on:click="selectOption()"
x-on:mouseenter="focusedOptionIndex = index"
role="option"
:aria-selected="focusedOptionIndex === index"
:class="{ 'text-white bg-gray-500': index === focusedOptionIndex, 'text-gray-900': index !== focusedOptionIndex }"
>
<span x-text="options[index].item.name"
:class="{ 'font-semibold': index === focusedOptionIndex, 'font-normal': index !== focusedOptionIndex }"
class="block font-normal truncate text-xs md:text-sm xl:text-base"
></span>
</div>
</template>
<div class="p-2 bg-white border-gray-300 focus:border-gray-500 cursor-not-allowed text-blue-400 text-xs md:text-sm xl:text-base"
x-show="ing === true">검색 중...</div>
<div class="p-2 bg-white border-gray-300 focus:border-gray-500 cursor-not-allowed text-red-400 text-xs md:text-sm xl:text-base"
x-show="options.length === 0 && input !== '' && ing === false">검색 결과 없음</div>
<div class="p-2 bg-white border-gray-300 focus:border-gray-500 cursor-not-allowed text-gray-500 text-xs md:text-sm xl:text-base"
x-show="(options.length === 0 || options.length === undefined) && input === '' && ing === false">검색 내용 입력 (1자리 이상)</div>
</div>
</div>
</div>
</div>
</div>