Search code examples
javascriptlaravelalpine.js

I got error making multiselect in laravel / livewire / alpinejs app


In laravel 10/ livewire 3/ tailwindcss 3 app I need to make multiselect working like select2, but without jQuery used, which is required by select2.

I found this https://github.com/alexpechkarev/alpinejs-multiselect code and try to implement in my blade file of simple livewire component I created with command :

php artisan make:livewire Test

I did not modify component file, but in blade file I edited :

<div class="admin_page_container" id="app_image_admin_page_container" x-cloak>

    <div class="w-full" x-data="alpineMuliSelect({selected:['te_11', 'te_12'], elementId:'multSelect'})">

        <select style="display:none;" id="multSelect">
            <option value="te_1" data-search="arsenal">Arsenal</option>
            <option value="te_3" data-search="Tottenham Hotspur Spurs">Spurs</option>
            <option value="te_3" data-search="Manchester City">Man City</option>
        </select>
    </div>
</div>

and I added js code into resources/js/app.js :

import './bootstrap';
import Alpine from 'alpinejs';

window.Alpine = Alpine;

import focus from '@alpinejs/focus'

import flatpickr from "flatpickr";


// I ADDED THESE LINES BEFORE Alpine.start() method
document.addEventListener("alpine:init", () => {
    window.Alpine.data("alpineMuliSelect", (obj) => ({
        elementId: obj.elementId,
        options: [],
        selected: obj.selected,
        selectedElms: [],
        show: false,
        search: '',
        open() {
            this.show = true
        },
        close() {
            this.show = false
        },
        toggle() {
            this.show = !this.show
        },
        isOpen() {
            return this.show === true
        },

        // Initializing component
        init() {
            const options = document.getElementById(this.elementId).options;
            for (let i = 0; i < options.length; i++) {

                this.options.push({
                    value:  options[i].value,
                    text:   options[i].innerText,
                    search: options[i].dataset.search,
                    selected: Object.values(this.selected).includes(options[i].value)
                });

                if (this.options[i].selected) {
                    this.selectedElms.push(this.options[i])
                }
            }

            // searching for the given value
            this.$watch("search", (e => {
                this.options = []
                const options = document.getElementById(this.elementId).options;
                Object.values(options).filter((el) => {
                    var reg = new RegExp(this.search, 'gi');
                    return el.dataset.search.match(reg)
                }).forEach((el) => {
                    let newel = {
                        value: el.value,
                        text: el.innerText,
                        search: el.dataset.search,
                        selected: Object.values(this.selected).includes(el.value)
                    }
                    this.options.push(newel);
                })
            }));
        },
        // clear search field
        clear() {
            this.search = ''
        },
        // deselect selected options
        deselect() {
            setTimeout(() => {
                this.selected = []
                this.selectedElms = []
                Object.keys(this.options).forEach((key) => {
                    this.options[key].selected = false;
                })
            }, 100)
        },
        // select given option
        select(index, event) {
            if (!this.options[index].selected) {
                this.options[index].selected = true;
                this.options[index].element = event.target;
                this.selected.push(this.options[index].value);
                this.selectedElms.push(this.options[index]);

            } else {
                this.selected.splice(this.selected.lastIndexOf(index), 1);
                this.options[index].selected = false
                Object.keys(this.selectedElms).forEach((key) => {
                    if (this.selectedElms[key].value == this.options[index].value) {
                        setTimeout(() => {
                            this.selectedElms.splice(key, 1)
                        }, 100)
                    }
                })
            }
        },
        // remove from selected option
        remove(index, option) {
            this.selectedElms.splice(index, 1);
            Object.keys(this.options).forEach((key) => {
                if (this.options[key].value == option.value) {
                    this.options[key].selected = false;
                    Object.keys(this.selected).forEach((skey) => {
                        if (this.selected[skey] == option.value) {
                            this.selected.splice(skey, 1);
                        }
                    })
                }
            })
        },
        // filter out selected elements
        selectedElements() {
            return this.options.filter(op => op.selected === true)
        },
        // get selected values
        selectedValues() {
            return this.options.filter(op => op.selected === true).map(el => el.value)
        }
    }));
});


Alpine.plugin(focus)
Alpine.start();

But I got error :

livewire.js?id=5d8beb2e:1192 Alpine Expression Error: alpineMuliSelect is not defined

Expression: "alpineMuliSelect({selected:['te_11', 'te_12'], elementId:'multSelect'})"

Not sure in which way have I to describe alpineMuliSelect method?

UPLOADED EXAMPLE :

I uploaded test app with this example at https://github.com/sergeynilov/TestApp

Standart laravel/livewire app. To instal alpinejs I used command

npm install alpinejs

Running server with command :

php artisan serve 

I open home page at http://127.0.0.1:8000 where I put select input

I created a component :

php artisan livewire:make HomePage  

Added small count component just be sure that alpine works ok
I left misspeling in “alpineMuliSelect” function .

In resources/js/app.js I added definition of “alpineMuliSelect”. I got the same error as on my page.

UPDATED INFO : I removed alpinejs from package.json and all alpinejs code from resources/js/app.js

Now I do not have any errors in console, but select input is not visible and checking in browser I see it has

style="display:none;"

enter image description here from

I added debugging in resources/js/app.js :

import './bootstrap';
document.addEventListener("alpine:init", () => {
    console.log('resources/js/app.js ::')  // I SEE THIS MESSAGE
    window.Alpine.data("alpineMuliSelect", (obj) => ({
        elementId: obj.elementId,
        options: [],
        selected: obj.selected,
        selectedElms: [],
        show: false,
        search: '',
        open() {
            console.log('resources/js/app.js OPEN::')
            this.show = true
        },
        close() {
            console.log('resources/js/app.js CLOSE::')
            this.show = false
        },
        toggle() {
            this.show = !this.show
        },
        isOpen() {
            console.log('resources/js/app.js isOpen::')
            return this.show === true
        },

        // Initializing component
        init() {
            const options = document.getElementById(this.elementId).options;
            console.log('resources/js/app.js init options::') // I SEE THIS MESSAGE
            console.log(options)
            for (let i = 0; i < options.length; i++) {
                ...

and checking output I see that alpineMuliSelect is triggered

enter image description here

After I removed

style="display:none;"

from select input - in this case I see usual select input visible.

Any ideas ?


Solution

  • I had a bit of a run with your code. Pulled locally and I was able to recreate your issue.

    TL;DR: Alpine Js seems to be re-initiating itself and didn't know how to handle the registration of your component.

    Understanding this Github discussion led me closer to the problem you were having. Worth a quick look to see how Alpine works.

    Now to go in-depth:

    Alpine Js is registered in the app already. I would assume it comes bundled with Livewire out of the box. Therefore, in your app.js, remove every form of Alpine Js initialization there. You don't have to import it as well.

    Essentially, the only thing that should be in your app.js is:

    import './bootstrap';
    

    You should no longer see an error (this is something you already tried earlier but you were unsure I think.

    The next problem you faced was having the select box hidden because of style="display:none;" attribute. If you go by the original code of the alpinejs-multiselect project, you will find that this is the expected behavior. The select box is really not displayed, as the creator used class="hidden" as well. This is because another input box referenced by:

    <input name="teams[]" type="hidden" x-bind:value="selectedValues()">
    is what displays the selected options. The selectbox just holds values for Alpine to manipulate.

    Although, I moved the Alpine.data bit to the blade file just to get things working, I believe you can move it back to your app.js and things should work just fine.

    Lastly, I would advise that you clone the code of the original alpine multiselect project, then remove the parts you don't need. I did that and it worked fine within your project.

    I am also making a PR that proves all what I've said, to give you a headstart. Hopefully, this works and you're able to carry on with your project!

    PR: https://github.com/sergeynilov/TestApp/pull/1

    Again, in case you missed it the first time, try to pull in the entire multiselect code from the originator and style the way you want. All your worries should be gone that way!

    Cheers!