gabrielesbaiz/nova-ajax-select
Nova Ajax Select is a Laravel Nova field that adds AJAX-powered searchable select inputs, loading options asynchronously so forms can handle large datasets without bloating the page. It supports remote search, custom endpoints, and a smooth Nova-native experience.
Before anything else, check for the asset collision. If alexwenzel/ajax-select is anywhere in your composer.json (directly or as a transitive dep), remove it first:
composer remove alexwenzel/ajax-select
php artisan nova:publish # or at minimum clear Nova's compiled assets
Both packages register the same asset handle, and Nova resolves scripts with ->first(). The failure mode is silent and confusing: no PHP error, no console error, just [Vue warn]: Unknown custom element and a field that renders as an empty box. If you are migrating an app that had both installed, assume one of your two ajax fields has been dead for a while.
Then:
composer require gabrielesbaiz/nova-ajax-select
Requirements are steep and worth confirming up front: PHP 8.3+, Laravel 12 or 13, Nova 5. Nova is licensed separately, so this is not a drop-in for an app still on Laravel 11.
Where to read first: the Guide for a fresh install, but if you have any 1.x field in the codebase, read the Upgrade page before touching code — it is mostly a list of things to delete, and deleting them is the point of the release.
First use case to reach for: the classic "big select" that 1.x users hacked around — a BelongsTo whose options are too numerous to ship in the form payload, currently backed by a Text field for the ID plus a shadow Text field to show the label on index. In 2.0 that becomes one field: resolver scoped to the parent, server-side search, and labels on index/detail out of the box.
Choose the resolver by shape, not by habit.
| Source | Use when |
|---|---|
| Closure | Small, dynamically filtered set; you need request/user context |
| Eloquent model | Large table, needs server-side search over named columns |
| Relation | Options are genuinely scoped to a parent record (e.g. $tenant->users()) |
| Backed enum | Fixed vocabulary — no DB round trip at all |
The enum resolver is underrated: it gives you Nova labels and validation for free and removes a config table you probably did not need.
Cascading selects — the headline change. In 1.x, parent() was a bespoke Vue watcher crawling the component tree, which is why it never fired inside action modals. It now registers a proper Nova dependency, so it composes:
Select::make('Country'),
NovaAjaxSelect::make('City')
->dependsOn(['Country']) // preferred in 2.0
...
Prefer dependsOn() over parent() even though parent() still works — it is deprecated for 3.0, and it is the difference between a field that survives being moved into a panel and one that quietly stops refreshing.
Panels, pivot forms and action modals now just work. This is the release's real payoff. Delete the workarounds: the duplicated field definitions inside BelongsToMany pivot blocks, the "action modal variant" of the same field, and the custom action classes that re-declared fields solely to get a cascading select. Likewise delete canSee() shims that only existed because dependsOn() never fired — leaving them in place now hides fields the dependency would have shown.
Labels on index and detail. Drop the shadow Text field and the accessor. The field carries {value, display}, so index shows the label rather than the foreign key. Migrate the column over in one deploy rather than leaving both, or you will spend a sprint explaining why one resource shows "Acme Ltd" and its sibling shows 42.
Validation comes for free — stop materializing Rule::in. 2.0 validates submissions with a single membership query. That means you should delete:
Rule::in(Pluck::from(...)) rules,sometimes|exists: rules layered on top,withValidator closures that existed to keep those in sync with the option list.The membership query respects the resolver's scope, so validation and options cannot drift. That is a real correctness win for tenant-scoped fields, where a hand-rolled Rule::in was one cache warm away from accepting another tenant's ID.
Server-side search: name your columns deliberately. Search runs on the DB against the columns you specify, which is what makes option sets of 50k rows viable. Name the human-facing columns (name, email, reference) and include the ID column only if users actually search by number — a broad column list turns every keystroke into a slow LIKE scan.
Caching is opt-in, and cacheScope() is not optional for multi-tenant. Turn caching on for stable option sets (enums, country lists, category trees) and always call cacheScope() with the tenant/user key when the resolver depends on it. Without it you will serve one tenant's filtered options to another — the kind of bug that only shows up in a demo to a customer.
No routes: stop writing them. 1.x exposed endpoints for option fetching. 2.0 rides Nova's own field-sync request, which is already authorized by Nova and already carries the request context. Anything you built as a custom endpoint, custom middleware, or canSee() gate around those routes is dead code — and if you wired rate limiting or audit logging to them, that telemetry disappears silently.
composer show for alexwenzel/ajax-select, and if present, remove, republish, hard-refresh. Nova's ->first() resolution means the loser fails without a trace.parent() works but is on the clock. 1.x calls compile fine, so there is no upgrade pressure — which is exactly how a deprecated API survives to become a 3.0 emergency. Sweep the codebase for parent() now; it is a one-line change per field.{value, display} payload is deprecated. If you have custom Vue components or JS that reads the field payload directly, it still works but will break at 3.0. Anything reading .display off a raw sync response is a ticking clock.get() is deprecated but still functional. New code should use the current resolver API; old ->get(fn () => ...) closures keep working. Do not mix styles within a single resource — the deprecated path bypasses some of the new caching and validation wiring.cacheScope() key is colliding. The absence of routes means there is no network tab entry to inspect; read Nova's field-sync payload instead.BelongsTo will make options vanish rather than 403.readonly() plus a dependency: the field now participates in Nova's dependency system, so a readonly() field with a dependent select can briefly hold a stale value when the parent changes. Reset it explicitly in your resolver if the form allows the parent to move after the child is set.composer why-not before planning the sprint, not during it.How can I help you explore Laravel packages today?