You're correct: the issue is that Livewire re-renders the <select>, destroying and recreating the Choices.js instance, which resets the UI state. To solve this, you need to prevent Livewire from touching the DOM for the select element, and handle syncing yourself.
Here’s a robust approach:
1. Use wire:ignore on the <select>
This tells Livewire to leave the DOM alone for this element, so Choices.js can manage it.
2. Sync Choices.js selections with Livewire manually
Use Alpine’s $watch or event listeners to update Livewire when the selection changes, and also watch for changes from Livewire to update Choices.js.
Here’s a working example:
<select
{{ $attributes->whereDoesntStartWith('wire:model') }}
x-data="choicesComponent(@entangle($attributes->whereStartsWith('wire:model')->first()).defer)"
x-ref="selectElement"
multiple
wire:ignore
>
@foreach ($options as $key => $value)
<option value="{{ $key }}">{{ $value }}</option>
@endforeach
</select>
@pushOnce('scripts')
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('choicesComponent', (entangledSelections) => ({
choices: null,
selections: entangledSelections,
init() {
// Initialize Choices.js
this.choices = new Choices(this.$refs.selectElement, {
removeItemButton: true,
searchEnabled: true,
placeholderValue: '{{ $placeholder }}',
});
// Set initial value from Livewire
this.$nextTick(() => {
this.choices.setChoiceByValue(this.selections);
});
// Listen for user changes and sync to Livewire
this.$refs.selectElement.addEventListener('change', () => {
const values = Array.from(this.$refs.selectElement.selectedOptions).map(option => option.value);
this.selections = values;
});
// Watch for Livewire changes and update Choices.js
this.$watch('selections', (value) => {
// Prevent infinite loop
if (JSON.stringify(this.choices.getValue(true)) !== JSON.stringify(value)) {
this.choices.removeActiveItems();
this.choices.setChoiceByValue(value);
}
});
}
}));
});
</script>
@endPushOnce
Key Points:
wire:ignoreprevents Livewire from re-rendering the select.- Use Alpine’s
$watchto update Choices.js when Livewire changes. - Use Choices.js events to update Livewire when the user changes the selection.
- Use
@entangle(...).deferfor two-way binding with Alpine and Livewire.
This pattern is the most reliable way to integrate Choices.js (or any JS UI library) with Livewire.
If you need to update the options list dynamically, you’ll need to handle that separately (e.g., by destroying and re-initializing Choices.js when options change).
Let me know if you need a more advanced example!