josedaian/select2wire
Select2Wire is a Laravel Livewire wrapper for the Select2 dropdown library. It lets you render searchable, AJAX-powered select inputs as Livewire components using simple Blade tags, wire:model binding, and remote data loading, without writing custom JavaScript.
Install and confirm the surface area first. The package ships one patch release (v2.0.1, "Update composer requirements") and a one-line description — there is no README to trust, so treat the source as the documentation.
composer require josedaian/select2wire
php artisan vendor:publish --provider="Josedaian\Select2Wire\Select2WireServiceProvider"
Then read, in this order:
composer.json — its illuminate/* and php constraints tell you whether it will even resolve against your Laravel version. This is the only thing v2.0.1 changed, so verify direction (loosened vs. tightened) before anything else.src/Select2WireServiceProvider.php — what it registers, what it publishes (config, views, assets), and whether assets are loaded automatically or must be @vite/@pushed.resources/views/ — the Blade component/view is the real public API. Component name, prop names, and whether it wraps a native <select> or builds one are all defined here.config/select2wire.php — defaults for version, theme, AJAX route, and any global options.First use case: a searchable user picker. Take the highest-cardinality select in your app (users, customers, products) and replace a <select> that renders thousands of <option> tags with a remote-backed one bound to a public property.
// Livewire component
public ?int $userId = null;
protected $rules = ['userId' => 'nullable|exists:users,id'];
{{-- Names are illustrative — copy the exact component/prop names from resources/views --}}
<livewire:select2wire.select
wire:model="userId"
:url="route('users.search')"
placeholder="Search users…"
/>
The backing endpoint is plain Select2 AJAX and follows Select2's stable contract, so you can build it independently of the package:
Route::get('/users/search', fn (Request $r) => [
'results' => User::query()
->when($r->q, fn ($q, $s) => $q->where('name', 'like', "%{$s}%"))
->limit(20)->get(['id', 'name as text']),
'pagination' => ['more' => false],
]);
Get this one flow working end-to-end — select a user, submit, assert the property persisted — before generalising to multi-select or tagging. Validation, wire:model, and the exists rule should work unchanged; if they don't, stop and read the view before writing more components.
Two-way binding with wire:ignore. Select2 mutates the DOM that Livewire also wants to morph. The reliable pattern is wire:ignore on the wrapper so Livewire never re-renders the <select> subtree, plus an explicit re-sync when the property changes from the server side. The package exists to remove the JS glue, but confirm from the compiled Blade whether it already emits wire:ignore — if not, add it yourself and re-initialise on $this->dispatch(...).
Remote vs. local options. Use remote AJAX for anything that can exceed a few hundred rows; use a plain $options array for small enums (statuses, categories) where an extra round-trip is wasted latency. Mixing the two in one form is normal — don't force every select through the AJAX path.
Multiple and tagging modes map to array properties. public array $tagIds = [], validated with ['tagIds' => 'array', 'tagIds.*' => 'exists:tags,id']. Tagging mode typically creates the entity on the backend from the search endpoint, so treat it as a write operation and authorise it — the select widget will happily let a user create records.
Cascading selects. Parent change → clear the child property → dispatch a browser event → child re-initialises with a new :url carrying the parent id. Do the clearing in updated() so dependent state never goes stale:
public function updatedCountryId(): void
{
$this->cityId = null;
$this->dispatch('select2-refresh');
}
Search endpoints should be thin. Query param name (q vs search), page param, and result shape are conventions, not configuration — pick the ones your package view uses and keep every endpoint identical so the front end never needs special cases.
Validation and form flow. Selected ids arrive as strings; PHP will coerce for exists, but in:/enum casts and strict comparisons will bite. Normalise in a cast or a small normalizeIds() helper rather than sprinkling (int) casts through views.
Script loading order is the top failure mode. Select2 4.x depends on jQuery. If Select2 loads before jQuery, or if Livewire's wire:navigate swaps the page without re-running your @push scripts, you get a silently non-interactive select. Register initialisation on both DOMContentLoaded and livewire:navigated, and check the browser console for $ is not defined before debugging anything else.
Pre-selected values must exist as server-rendered <option>s. Select2 will not display "Jane Doe" for userId = 5 until the remote fetch resolves unless the view renders an <option value="5" selected>. Missing this looks like a binding bug but is a rendering-order issue — and it's the most common "my edit form shows blank" report.
Empty string vs. null. Select2 emits "" when a selection is cleared. With ?int typed properties that's a TypeError; use a null sentinel, whereNotNull filters, or hydrate via filled(). Also beware id 0, which is falsy in more places than you'd like.
Debounce and minimumInputLength. Set both on the widget and enforce a floor server-side too — a keypress-per-request search endpoint on a large table is the classic way a "small UI package" becomes a database load problem. Cap result size and add an index on whatever you LIKE.
Verify the maintenance story before going deep. Last release November 2023, zero stars, and v2.0.1 is a composer-only diff. That means: pin an exact version in composer.lock, and assume no upstream fixes. Budget for vendoring resources/views and the JS init via vendor:publish so local overrides survive, and prefer forking over filing issues if you need changes. Run composer update --dry-run --with-dependencies on a branch first — a loosened constraint can silently resolve a different transitive jQuery/Alpine stack without any code diff.
Debugging checklist, in order: (1) is the <select> in the DOM after hydration; (2) is Select2 actually attached ($('#id').data('select2')); (3) does the network tab show your search request with the expected q and page; (4) does the JSON match {results:[{id,text}], pagination:{more}}; (5) is the property updated in the Livewire payload. Ninety percent of issues resolve at step 2 or 4.
Extension points. The published config is the supported customisation surface; the published Blade view is the escape hatch for anything the config doesn't cover (custom templateResult, badges, "load more" footers). Both are yours to edit after publishing — which is exactly why you should publish early rather than relying on upstream updates that may never arrive.
How can I help you explore Laravel packages today?