livewire 에서 alpine 으로 select 만들기

Updated on

                                                                    <div wire:ignore>
                                                                        <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>

3일.. 3일이란 시간이 걸렸다. 난 정말 미친놈이다.

처음에 만들었을때에는 overflow 문제로 css 해결이 안되서 좌절… (전 포스팅)
두번째에는 alpine 플러그인을 통해서 야메로 작업해서 select2 처럼 body 맨 하단에 위치하게끔 두고서, select 모듈 처럼 두고서 사용하려고 생각해서 제작.

https://codepen.io/jskorlol/pen/NWRXLzm

https://github.com/alpine-collective/alpine-magic-helpers/issues/63#issuecomment-751909848

응 안돼 돌아가 ^^.

self 로 하면, 버그가 있던거 같다… 어쨌든, 코드 재활용을 하려면 따로 script 로 빼고서… 쓰면 좋겠는데.

    value: @entangle('company_id'),

때문에 어캐해야할지 모르겠다.

일단…. 3일만에 성공했따… 총 세번째 작업에서 성공한건데..
사실 2번째 시도는 나름 신선하기도 했고, 이슈도 찾아내서 이슈가 fix될진 모르겠지만..
1번째 시도에서 css 설정에서 몇가지 값만 바꿔주면 해결되는 문제였다. (근데 좀 버그가 있기는 함… 라운드 처리되는게 씹히는 버그?) <추가적으로 resize 이벤트를 넣어서, fixed 된 search listbox 의 크기를 실시간 조절 해주었음>

어쨌든, 작동하기만 하면된다. 안되면 나중에 고치는거로 하고..

처음에는 도큐먼트로 엘리먼트 찾아서 했는데.

https://github.com/danharrin/alpine-tailwind-components

보면서 많이 따라했다. 굿.

이게 진짜 장점이. body 가 밖으로 나가 있는 녀석은 select2 처럼 스크롤을 막아줘야한다.
즉, 검색결과가 열려있을때에는 외부 스크롤휠을 javascript 로 차단시켜주어야한다.

하지만, 지금처럼 함께 있는 녀석은 굳이 스크롤을 차단시켜주지 않아도 된다.

select2의 경우 아래 공간이 없으면 위로 검색바를 생성시켜주는데, 위로 생성된 애는 검색결과 창에 대해서 위치 버그? 같은게 있긴하던데…
어쨌든 나는 그정도 까지 들어가고싶진 않다.

자 빨리 다음 작업으로 넘어가야겠다. 굿.