drpshtiwan/livewire-async-select
A lightweight async select component for Laravel Livewire and Alpine.js, offering a modern Select2 alternative with dynamic option loading, debounced search, multi-select chips, Tailwind styling, custom slots, and full wire:model binding.
Architecture fit. The package occupies a narrow, well-understood slot in a Laravel/Livewire codebase: a native Livewire child component that behaves as a form input, not a framework or an abstraction layer. It binds through wire:model two-way binding, delegates client interactivity to Alpine.js, and fetches options from an application-owned HTTP endpoint returning {"data": [...]}. There is no new persistence model, no queue, no container override, no event bus. For a team already on Livewire 3.3+/4.x with Tailwind, the architectural question collapses to one decision: where does the search endpoint live, and who owns it? Two integration surfaces exist: (a) a layout-level dependency — @asyncSelectStyles in <head>, @livewireStyles/@livewireScripts, @stack('scripts') before </body>, plus a published CSS asset; and (b) the search endpoint itself. The las- class prefix is a genuine architectural courtesy — it means the component will not collide with an existing Tailwind config or design system, which is precisely where component adoption usually stalls. The Bootstrap 4 theme added in 3.2.0 signals the author intends theme-agnosticism rather than a Tailwind lock-in.
Integration feasibility. High for a single team, moderate as a platform standard. The happy path is four steps: composer require, vendor:publish --tag=async-select-assets, three Blade directives, then one <livewire:async-select> tag per field. The endpoint is a five-line closure or a thin invokable controller. Authentication is the one non-trivial piece: the package ships an async-auth middleware that behaves like Laravel's auth but additionally handles an internally-minted signed-token path (X-Internal-User header) with replay protection, and supports web, sanctum, and api guards including an optional persist flag for session continuity. Custom rendering uses Livewire named slots on 3.3+/4.x, with callable slots retained as a backwards-compatible fallback — evidence the author absorbed a Livewire slot-API change instead of pushing it downstream. Total estimated integration effort for a first field on an existing Livewire app: 0.5–1 dev-day, dominated by the endpoint and auth wiring, not the component.
Technical risk. Four risks, roughly ordered by severity:
tests/Feature/CustomSlotsTest.php) — but it means the field-testing surface is small. Every bug we find is likely a bug nobody has hit before.X-Internal-User mechanism is a security-relevant invention layered on top of Laravel auth. It needs an explicit security review before it goes anywhere near a production permission boundary. Do not treat "auth supported" as "auth vetted."Key questions.
Stack fit. Strong, conditional on the house stack already being Laravel 11+ / PHP 8.2+ / Livewire 3.3+ / Tailwind. That is the median modern Laravel app, so fit is broad. The Alpine dependency is effectively free — Livewire ships Alpine, so the component adds no new client runtime. The only genuinely new runtime surface is the published CSS file and the search endpoint. If the application is server-rendered Blade with no Livewire, this package is not the right tool; reach for a plain Alpine combobox instead.
Migration path. Frame this as additive, not as a replacement project. The realistic migration is per-form, replacing one of three incumbent patterns: (a) a full-options <select> that has grown past a few hundred rows and is now slow to render; (b) a Select2/jQuery widget that fights Livewire re-renders; (c) a hand-rolled Alpine dropdown with no debounce. Do not attempt a codebase-wide sweep. Start with one low-stakes field — a tag picker, a category selector — validate the endpoint contract and the styling in situ, then template it.
Compatibility. Hard requirements: PHP 8.2+, Laravel 11/12/13, Livewire 3.3+/4.x. Three compatibility hazards to check before committing:
@stack('scripts') is mandatory and silently load-bearing. Omit it and the component renders but never initialises. Add it to the base layout and add a smoke test or a checklist item so a future layout refactor doesn't quietly delete it.vendor:publish --tag=async-select-assets drops CSS into public/; it will not pass through Vite, PurgeCSS, or versioned asset hashing. Decide where it lives and how it is deployed (checked into the repo vs. published at deploy time) and document it.Sequencing.
las- styling under our design tokens.async-auth middleware and decide explicitly whether we use internal signed tokens, plain sanctum, or plain web + session. Default to the guard we already trust.<x-async-select> that pins props, endpoint naming, error handling, and the slot conventions. This is the single highest-leverage step — it is what converts a 112-star dependency into an internally-owned interface with a two-day swap cost.@stack('scripts') requirement; then roll out to further forms opportunistically, not as a migration project.Maintenance. Low ongoing engineering cost, high attention cost. There is nothing to run — no service, no daemon, no schema. The work is version bumps. Budget roughly 1–2 dev-days per year for upgrade cycles, concentrated on Livewire majors. Because the coupling is to Livewire itself, the real cost lands whenever Livewire cuts a major, which is also when the rest of the app is in flux. Pin the version in composer.json and upgrade deliberately rather than floating.
Support. This is the weakest dimension. Support model is GitHub Issues plus Discussions on a solo-maintained repo — no SLA, no commercial backing, no security disclosure policy evident in the README. For an internal-tools or back-office surface, that is acceptable. For a customer-facing checkout flow, it is not, unless we accept full internal ownership of the component's source (which, being MIT and a handful of Blade files, is entirely feasible — vendoring or forking is a realistic hedge). Set the expectation up front with stakeholders: we are adopting a component, not a vendor.
Scaling. The component itself is client-side and cheap; the scaling profile is entirely determined by the endpoint we write. Each keystroke after the debounce is one HTTP request, and each open dropdown is one request. Capacity math is straightforward: (active select interactions per minute) × (keystrokes after debounce). Two obligations fall on us, not the package:
WHERE name LIKE '%term%' with LIMIT 20 against a million-row table is a table scan per keystroke. Use prefix search, a trigram index, or a search service, and cap results server-side. A max-selections cap keeps payload size predictable on multi-select.throttle:60,1 or similar) so a stuck client or a scripted abuser cannot turn the search endpoint into a load generator.On the client, dozens of async selects on one page would be a rendering concern; in practice select fields appear a handful at a time per form, so this is not a realistic ceiling.
Failure modes. Ranked by likelihood × blast radius:
@stack('scripts'), a CSP blocking inline scripts, or an Alpine version conflict → the select renders as a dead box. Detected instantly by users; cause is non-obvious. Mitigate with a smoke test on a canonical form.las- classes with no rules. Cheap to detect, cheap to fix, but easy to miss in staging.Ramp-up. Low. Any developer fluent in Livewire can read a <livewire:async-select> tag and infer its behaviour; wire:model binding and Blade slots are existing mental models, not new ones. Expect ~1 hour to first productive use, plus ~2 hours of reading for the auth middleware and custom-slot syntax — the two non-obvious pieces. The las- prefix and the Select2-parity feature set mean no design-system retraining. The ramp-up cost is not in learning the component; it is in learning our conventions around it. That is why the wrapper component in sequencing step 3 matters more than any documentation the vendor ships: it moves the ramp-up burden from every developer to the platform owner, and it is the mechanism that keeps the exit door open.
How can I help you explore Laravel packages today?