DEV Community

Kieran Fahy
Kieran Fahy

Posted on

Livewire 3 Multiple Select with Alpine JS

I have built a multiple select dropdown using livewire 3 and alpine. Most of the code I found on the web and adjusted it to work for me. However, when I adding the model I am setting it by the property name rather then using wire:model and @entangle(). I'm wondering if there is a better way to sync between the component than what I'm currently doing

@props([
    'optionValue'  => '',
    'optionLabel' => '',
    'model',
    'optionsList' => [],
    'selectedOptions' => [],
    'label' => null,
    'disabled' => false,
])

<div wire:ignore>

    @if($label)
        <x-input-label class="mb-1" for="{{ $model }}">{{ $label }}</x-input-label>
    @endif

    <select style="display: none;" id="select">
        @foreach($optionsList as $optionList)
            <option
                value="{{ $optionList[$optionValue] }}" {{ in_array($optionList[$optionValue], $selectedOptions) ? 'selected=selected' : false }}>
                {{ $optionList[$optionLabel] }}
            </option>
        @endforeach
    </select>

    <div x-data="dropdown()" x-init="loadOptions()">
        <input name="values" type="hidden" x-bind:value="selectedValues()">
        <div class="inline-block relative w-full">
            <div class="flex flex-col items-center relative">
                <div x-on:click="open" class="w-full">
                    <div class="flex form-select-sm appearance-none block w-full px-2 py-1 text-sm
                                                                font-normal
                                                                text-gray-700
                                                                bg-white bg-clip-padding bg-no-repeat
                                                                border border-solid border-gray-300
                                                                rounded
                                                                transition
                                                                ease-in-out
                                                                duration-500
                                                                focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none">
                        <div class="flex flex-auto flex-wrap">
                            <template x-for="(option,index) in selected" :key="options[option].value">
                                <div
                                    class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 mr-1 text-sm font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
                                    <div class="text-xs font-normal leading-none max-w-full flex-initial"
                                         x-model="options[option]" x-text="options[option].text"></div>
                                    <div class="flex flex-auto flex-row-reverse">
                                        <div x-on:click="remove(index,option)">
                                            <svg class="fill-current h-4 w-4 " role="button" viewBox="0 0 20 20">
                                                <path d="M14.348,14.849c-0.469,0.469-1.229,0.469-1.697,0L10,11.819l-2.651,3.029c-0.469,0.469-1.229,0.469-1.697,0
                                           c-0.469-0.469-0.469-1.229,0-1.697l2.758-3.15L5.651,6.849c-0.469-0.469-0.469-1.228,0-1.697s1.228-0.469,1.697,0L10,8.183
                                           l2.651-3.031c0.469-0.469,1.228-0.469,1.697,0s0.469,1.229,0,1.697l-2.758,3.152l2.758,3.15
                                           C14.817,13.62,14.817,14.38,14.348,14.849z"/>
                                            </svg>
                                        </div>
                                    </div>
                                </div>
                            </template>
                            <div x-show="selected.length == 0" class="flex-1">
                                <input placeholder="Select a option"
                                       class="bg-transparent p-1 px-2 appearance-none outline-none h-full w-full text-gray-800"
                                       x-bind:value="selectedValues()"
                                >
                            </div>
                        </div>
                        <div class="text-gray-300 w-8 py-1 pl-2 pr-1 border-l flex items-center border-gray-200">

                            <button type="button" x-show="isOpen() === true" x-on:click="open"
                                    class="cursor-pointer w-6 h-6 text-gray-600 outline-none focus:outline-none">
                                <svg version="1.1" class="fill-current h-4 w-4" viewBox="0 0 20 20">
                                    <path d="M17.418,6.109c0.272-0.268,0.709-0.268,0.979,0s0.271,0.701,0,0.969l-7.908,7.83
    c-0.27,0.268-0.707,0.268-0.979,0l-7.908-7.83c-0.27-0.268-0.27-0.701,0-0.969c0.271-0.268,0.709-0.268,0.979,0L10,13.25
    L17.418,6.109z"/>
                                </svg>

                            </button>
                            <button type="button" x-show="isOpen() === false" @click="close"
                                    class="cursor-pointer w-6 h-6 text-gray-600 outline-none focus:outline-none">
                                <svg class="fill-current h-4 w-4" viewBox="0 0 20 20">
                                    <path d="M2.582,13.891c-0.272,0.268-0.709,0.268-0.979,0s-0.271-0.701,0-0.969l7.908-7.83
    c0.27-0.268,0.707-0.268,0.979,0l7.908,7.83c0.27,0.268,0.27,0.701,0,0.969c-0.271,0.268-0.709,0.268-0.978,0L10,6.75L2.582,13.891z
    "/>
                                </svg>

                            </button>
                        </div>
                    </div>
                </div>
                <div class="w-full">
                    <div x-show.transition.origin.top="isOpen()"
                         class="absolute shadow top-100 bg-white z-40 w-full lef-0 rounded max-h-64 overflow-y-auto text-sm"
                         x-on:click.away="close">
                        <div class="flex flex-col w-full">
                            <template x-for="(option,index) in options" :key="index">
                                <div>
                                    <div
                                        class="cursor-pointer w-full border-gray-100 rounded-t border-b hover:bg-gray-200"
                                        @click="select(index,$event)">
                                        <div x-bind:class="option.selected ? 'border-gray-600' : ''"
                                             class="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative">
                                            <div class="w-full items-center flex">
                                                <div class="mx-2 leading-6" x-model="option" x-text="option.text"></div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </template>
                        </div>
                    </div>
                </div>
            </div>
            <!-- on tailwind components page will no work  -->
        </div>
    </div>
        <script>
            function dropdown() {
                return {
                    options: [],
                    selected: [],
                    show: false,
                    open() {
                        this.show = true
                    },
                    close() {
                        this.show = false
                    },
                    isOpen() {
                        return this.show === true
                    },
                    select(index, event) {

                        if (!this.options[index].selected) {

                            this.options[index].selected = true;
                            this.options[index].element = event.target;
                            this.selected.push(index);
                            @this.set('{{ $model }}', this.selectedValues());


                        } else {
                            this.selected.splice(this.selected.lastIndexOf(index), 1);
                            this.options[index].selected = false
                        }
                    },
                    remove(index, option) {
                        this.options[option].selected = false;
                        this.selected.splice(index, 1);
                        @this.set('{{ $model }}', this.selectedValues());
                    },
                    loadOptions() {
                        const options = document.getElementById('select').options;

                        for (let i = 0; i < options.length; i++) {
                            this.options.push({
                                value: options[i].value,
                                text: options[i].innerText,
                                selected: options[i].getAttribute('selected') != null ? options[i].getAttribute('selected') : false
                            });
                            if (options[i].getAttribute('selected') != null) {
                                this.selected.push(i);
                            }
                        }

                    },
                    selectedValues() {
                        return this.selected.map((option) => {
                            return this.options[option].value;
                        })
                    }
                }
            }
        </script>
</div>

Enter fullscreen mode Exit fullscreen mode

I then add it to my livewire component like so

<x-form.multi-select
                            model="schoolsListForm.grades"
                            label="Grades"
                            :optionsList="\Config::get('applications.grades')"
                            :selectedOptions="$schoolsListForm->grades"
                            optionValue="id"
                            optionLabel="name" />
Enter fullscreen mode Exit fullscreen mode

Thanks

Top comments (0)