alexwenzel/ajax-select
A Laravel package for AJAX-powered select inputs that let users search and load options from a remote endpoint as they type, handy for large datasets like users or products where preloading every option isn't practical.
alexwenzel/ajax-select is a Laravel Nova form field that renders a <select> whose options are fetched over HTTP at render time instead of being serialized into the Nova index/show payload. Its secondary purpose is the "child select" — a select whose option list is reloaded whenever another field on the same form changes (the classic Country → City / Category → Subcategory cascade).
Minimal setup:
composer require alexwenzel/ajax-select
php artisan nova:publish # only if the package ships views/assets you override
php artisan optimize:clear # config + view cache, so Nova picks up the new field
You need Nova installed and a working nova guard first — this package is a field, not a standalone tool, so nothing works without a Nova resource to hang it on.
Then add the field to a resource:
use Alexwenzel\AjaxSelect\AjaxSelect;
public function fields(NovaRequest $request)
{
return [
AjaxSelect::make('Category', 'category_id')
->endpoint(route('api.categories.index')) // returns [{value, label}, ...]
->rules('required', 'exists:categories,id'),
];
}
Your endpoint is just a normal route returning JSON:
Route::get('/api/categories', fn () => Category::query()
->when(request('q'), fn ($q, $term) => $q->where('name', 'like', "%{$term}%"))
->limit(50)
->get(['id as value', 'name as label']));
Look first at the field class in src/ (for the fluent API: endpoint, dependsOn, searchable, placeholder, etc.) and the compiled component in dist/js. The README is thin for a package this size, so reading the field class is faster than guessing.
First use case: a two-level cascade on a create form — a plain Select for the parent, an AjaxSelect for the child. It gives you the "child list reloads" behavior without writing a custom Vue field.
Parent/child cascade. The parent stays a normal Nova Select (its options are small and static). The child declares the dependency and the endpoint reads the parent value from the request:
Select::make('Country', 'country_id')->options(Country::pluck('name', 'id')),
AjaxSelect::make('City', 'city_id')
->endpoint(route('api.cities.index'))
->dependsOn('country_id'), // prop name on the parent field
Route::get('/api/cities', fn (Request $request) => City::query()
->where('country_id', $request->query('country_id'))
->get(['id as value', 'name as label']));
Keep the query-parameter key identical to the parent field's attribute name — that string is the contract between the two fields, and it's the single most common thing to get wrong.
Search-as-you-type for large tables. For anything over a few hundred rows, don't dump the whole table. Use the search term, limit(50), and index the column you filter on. This is the pattern that justifies the package: a BelongsTo on a 200k-row users table will otherwise bloat every form render with a giant <option> list.
Edit forms need preloaded labels. On update, the field has no options until the first fetch resolves, so the current value renders blank or as a raw ID. Either:
options()/withMeta() for the current value, or->displayUsing(fn ($id) => User::find($id)?->name) for index/detail views.Do this deliberately — reviewers notice a blank required field on the edit screen.
Endpoint reuse with a type discriminator. One controller for many selects:
Route::get('/api/options/{type}', OptionsController::class);
Switch on $type inside the controller. Fewer routes, one auth policy, one place to add caching.
Validation lives in the field rules, not the endpoint. Ajax only drives the UI; the browser can post anything. Always pair with ->rules('exists:...') (or a Rule::in closure). Treat the endpoint as a hint, the rule as the guarantee.
Tenant / permission scoping. Every endpoint must filter by the authenticated user's tenant. An Ajax endpoint is effectively a small read API — a user who can't see a record in Nova shouldn't be able to enumerate it via ?q=.
Testing. Field-level tests use Nova::actingAs($user) and assert the route returns the right shape; the component itself is best covered with one Dusk smoke test of the cascade (change parent → child options change).
web group (session + CSRF) and behind your auth/nova guard. If it's in api, Nova's axios call will 401/419 and the select just silently shows nothing. Open the network tab before touching the Vue component.[{value, label}] exactly; if the component expects id/text, the select will be blank with a green response.city_id stays selected and gets saved against a different country. Set the child value to null on parent change.Cache::remember on the endpoint is a cheap win for hot, rarely-changing lists, but key it on the search term and the tenant, and invalidate on save. A cache keyed only on q is a cross-tenant data leak.composer.json Nova constraint against your Nova major, and lock it. Verify the compiled assets match your Nova version's build pipeline.->displayUsing() or ->hideFromIndex() and a real column via BelongsTo for sorting/filtering.BooleanFilter/SelectFilter that maps to the same column if you need it.component() and withMeta(), and the Vue component lives in resources/js. Forking or vendoring the component is realistic here — at this size you may end up maintaining it, so budget for that before adopting.How can I help you explore Laravel packages today?