techouse/select-auto-complete
A Laravel Nova field that upgrades standard select inputs with searchable, auto-completing dropdowns. It supports dependent fields, AJAX-powered remote options, and customizable queries, making large option lists fast and easy to navigate in your Nova admin panels.
Confirm the precondition first. Despite the description's talk of "Blade-based forms," every source classifies this as a Laravel Nova field — a PHP Field subclass plus a compiled Vue component mounted into the Nova SPA. Without a licensed Nova install it is inert. Before doing anything else, read the laravel/nova constraint in the repo's composer.json and compare it to the version you actually run. Nova 3 is Vue 2; Nova 4/5 is Vue 3 + Inertia. An archived repo has no fix path across that boundary, so a mismatch is a hard stop, not a "we'll patch it later."
Read, then decide. The package is archived, has 30 stars and zero dependents. The honest day-one move is a one-hour read of three files, not a composer require:
composer.json — Nova/Laravel/PHP constraints.src/*.php — the field class; note the fluent surface (whether searchUsing(), maxResults(), minSearchLength()-style methods exist) and, critically, how the search callback queries the model.dist/js/ vs resources/js/ — if the compiled bundle has drifted from source, the fork route is dead on toolchain reconstruction alone.If the API and constraints line up, install on a throwaway branch:
composer require techouse/select-auto-complete
php artisan nova:publish
Most Nova field packages ship a service provider that registers assets on Nova::serving. Check for one; if it's absent, register manually:
// app/Providers/NovaServiceProvider.php
public function boot()
{
parent::boot();
Nova::script('select-auto-complete', __DIR__.'/../../vendor/techouse/select-auto-complete/dist/js/field.js');
Nova::style('select-auto-complete', __DIR__.'/../../vendor/techouse/select-auto-complete/dist/css/field.css');
}
First use case. Pick the single worst offender in your admin panel — a BelongsTo on a table with thousands of rows that renders a giant <select>, or a static options list of hundreds of entries. Wire the field into that resource only, behind a feature branch, and measure. Do not roll it out resource-by-resource ahead of that measurement.
use Techouse\SelectAutoComplete\SelectAutoComplete;
public function fields(Request $request)
{
return [
SelectAutoComplete::make('Customer', 'customer_id')
->options(Customer::pluck('name', 'id')->all()), // static path, for comparison
];
}
That static path is your baseline: it proves the component mounts and renders. The real value is the async path below — verify the exact method names against src/ before writing them; the fluent surface is small but the archived repo is the only spec.
Async search with a scoped, indexed query. This is the pattern the package exists for, and the one you must audit. The callback should hit a narrow, indexed query — not Model::all() filtered in PHP, and not an unbounded LIKE:
SelectAutoComplete::make('Customer', 'customer_id')
->searchUsing(function (Request $request) {
$term = trim((string) $request->input('search'));
if (mb_strlen($term) < 2) {
return []; // min query length: protects the DB from 1-char scans
}
return Customer::query()
->where('name', 'like', $term.'%') // prefix LIKE uses the index
->orderBy('name')
->limit(20) // hard result cap
->get(['id', 'name']) // never hydrate full rows
->map(fn ($c) => ['value' => $c->id, 'label' => $c->name])
->all();
})
->maxResults(20);
Two things matter more than the syntax: the prefix LIKE (not %term%) so the index on name is usable, and an explicit limit so a stray keystroke can't pull 50k rows. Nova's own BelongsTo searchable field already does a version of this — benchmark it side by side before adopting a dependency for it.
Multi-tenant / policy scoping — never skip this. The search endpoint is an AJAX route that returns record labels. If it queries the model directly, suggestions leak record names across tenants. Scope inside the callback using the authenticated user, not a global scope you're assuming fires:
->searchUsing(function (Request $request) {
$term = trim((string) $request->input('search'));
if (mb_strlen($term) < 2 || ! $request->user()->can('viewAny', Customer::class)) {
return [];
}
return Customer::query()
->where('tenant_id', $request->user()->tenant_id) // explicit, not implicit
->where('name', 'like', $term.'%')
->limit(20)
->get(['id', 'name'])
->map(fn ($c) => ['value' => $c->id, 'label' => $c->name])
->all();
});
Write an HTTP feature test that asserts a user from tenant A gets zero results for tenant B's customer names. This is the one test that justifies the package's existence in a review.
Wrap it in your own field class. The single highest-leverage pattern for an abandoned package: subclass it once and use your class everywhere. When you later replace it with Nova's native field or an in-house implementation, the change is one file, not fifty resources.
// app/Nova/Fields/SearchableCustomer.php
namespace App\Nova\Fields;
use Techouse\SelectAutoComplete\SelectAutoComplete;
class SearchableCustomer extends SelectAutoComplete
{
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute ?? 'customer_id', $resolveCallback);
$this->searchUsing(fn ($request) => $this->search($request))
->maxResults(20);
}
protected function search($request): array { /* scoped query above */ }
}
Form-level conventions that keep it clean. Hide it on index/detail views where the async widget adds nothing (->hideFromIndex()->hideFromDetail()), add ->rules('required', 'exists:customers,id') because a custom component bypasses some of Nova's implicit relationship validation, and set ->nullable() deliberately rather than by omission. If the field replaces a real BelongsTo, remember you've traded away Nova's relationship plumbing for a raw ID — you own the display value on saved records, so confirm an existing record loads with the correct label and not a bare integer.
Testing the integration. A nova:publish-then-render smoke test on the target resource is worth more than unit tests here: assert the field appears on the form, that a known term returns the expected label, and that a term shorter than the minimum returns an empty set. Run it as a precondition to every Nova version bump.
Archived means you inherit it on day one. Pin an exact version ("techouse/select-auto-complete": "1.0.4", no caret), fork it under your org with a named owner, and record it as owned code with a dead upstream — not as a vendor dependency. There will be no CVE response and no compatibility patch for the next Nova major. That is the strongest single argument for reading the source as a spec and reimplementing a few hundred lines you control.
The description lies about the integration surface. "Blade-based forms" and "ordinary select inputs" do not match a Nova-locked PHP field + Vue SPA component. Do not attempt to use it in a Blade form; do not promise a stakeholder a non-Nova path exists.
Verify the search endpoint's authorization behavior before shipping. Read the route/handler in src/. Does it apply Nova's policy layer? Does it confine results to the caller's tenant? If it queries the model directly with no scope, suggestion leakage is a data-exposure vector, not a cosmetic bug. This is the item to escalate, not the styling.
Watch for performance regressions masquerading as improvements. You've moved work from page render to per-keystroke queries — better at high cardinality, but you've created a new hot path. Enforce debounce client-side (the component may or may not do it), set a minimum query length of 2+, cap results at ~20, and ensure a real index backs the searched column. Validate at production cardinality, never on seed data; a LIKE '%term%' that feels instant on 500 rows will table-scan on 500k.
Asset and component-name collisions are the classic failure mode. Nova field packages all inject into the same SPA. After install, load the target resource with every other field on it and watch the console for duplicate global component names or manifest conflicts. If you fork, rename the global component to something namespaced (yourorg-searchable-select) to avoid colliding with the original should it ever appear transitively.
dist/js drift kills the fork route. If the compiled bundle doesn't match resources/js and you can't reconstruct the build toolchain, you cannot fix a front-end bug — you can only work around it. Check this before committing to vendoring.
Silent breakage on Nova upgrades. The typical failure is not a crash: the field renders but search returns nothing, or the component fails to mount and you get a bare input. Without a smoke test on the target resource you'll discover this from a support ticket. Make the smoke test a gate on every Nova bump while the field is live.
The likely correct answer is that you don't need this package. Check current Nova's native searchable BelongsTo against your largest real option set first — the package's reason to exist may have evaporated. If you do need more, the fluent surface (searchUsing-style callback + maxResults + debounce) is the whole spec; reproduce it in a small in-house field and keep the dependency graph clean. If your roadmap question underneath this is really "is Nova the right admin stack," note that Filament ships searchable async selects natively — that's a bigger decision than this widget, but it's the one worth having.
How can I help you explore Laravel packages today?