ziffmedia/nova-select-plus
An enhanced Laravel Nova select field with searchable, AJAX-loaded options and dependent selects. It keeps resource forms fast when dealing with large option sets or dynamic relationships, using a familiar Nova field API.
Install
composer require ziffmedia/nova-select-plus
Pin it ("ziffmedia/nova-select-plus": "2.0.2"). No service provider registration or asset publishing is required — the field ships its compiled JS through Nova's asset pipeline. The package assumes Laravel Nova 4.x (and PHP 8.x); activity is low, so verify against Nova 5 before committing to it in a new project.
Version note (v2.0.2) — this is a patch on the 2.0.x line, not a breaking change: form field styling tweaks plus a fix for DependsOn integration on the Form.vue side. If you use ->dependsOn() for cascading selects, 2.0.2 is effectively the minimum version — see Gotchas.
Where to look first
README.md — the only real API surface; the field is a thin wrapper, so the method list is the documentation.src/SelectPlus.php — confirms the exact fluent methods, the component() name, and how options are serialized to the frontend.resources/js/components/ — the Vue side (Form.vue and friends); this is what you fork or patch when you need custom behaviour. The 2.0.2 DependsOn fix lives here, so read it before debugging a dependency that "isn't firing".routes/ — if the Ajax mode is present, this is where the options endpoint is defined and what middleware guards it.Minimal usage
use ZiffMedia\NovaSelectPlus\SelectPlus;
SelectPlus::make('Status')
->options([
'draft' => 'Draft',
'review' => 'In Review',
'published' => 'Published',
]),
Drop-in for Laravel\Nova\Fields\Select — same ->options(), same rules, same ->displayUsing() semantics.
First real use case — the one that justifies the dependency: a belongsTo-style foreign key stored as an integer, where the candidate list is too long for Nova's native select (thousands of users/customers/categories). Native Select renders every option into the DOM and becomes unusable; SelectPlus gives you a searchable/Ajax-backed picker over the same column without changing your schema.
Searchable static options
SelectPlus::make('Category')
->options(Category::pluck('name', 'id')->all())
->searchable(),
Search is client-side, so this only pays off up to a few hundred options. Beyond that, filter on the server (or move to Ajax mode).
Ajax-backed options
Point the field at a source and let it fetch on keystroke instead of hydrating the whole list in the form payload:
SelectPlus::make('Customer')
->resource(\App\Nova\Customer::class) // or an explicit endpoint
->label('name'), // attribute used for the visible label
Two things to verify against the README before you build on it: the exact method name for the resource/endpoint binding, and the shape the endpoint must return (typically [{ value, label }] or { results: [...] }). If you supply your own endpoint, keep it behind Nova's Authorize/nova middleware group so it inherits the panel's auth, and return results scoped to the same authorization rules as your resource index.
Cascading selects
The headline pattern is parent → child option reloads (Country → State, Client → Project):
SelectPlus::make('State')
->dependsOn('country')
->resolveOptionsUsing(function ($request, $values) {
// $values holds the current form state, including `country`
return State::where('country_id', $values['country'] ?? null)
->orderBy('name')
->pluck('name', 'id');
}),
Keep the resolver cheap — it fires on every parent change. Index the FK column, cache if the list is truly static, and always handle the "parent not yet selected" case by returning an empty array rather than null.
On 2.0.2 this wiring is fixed on the Form.vue side; if you built the pattern on an earlier 2.0.x you may have worked around a dependency that simply never fired. Re-test the cascade after upgrading rather than assuming your old workaround is still needed.
Standardizing the option source
Once you have more than three of these, stop repeating the resolver:
trait HasSelectPlusOptions
{
protected function optionsFrom(string $model, string $label = 'name'): callable
{
return fn () => $model::query()->orderBy($label)->pluck($label, 'id')->all();
}
}
Because SelectPlus extends Nova's Select, everything you already do keeps working — ->rules(), ->nullable(), ->default(), ->help(), ->onlyOnForms(), and field-level ->canSee().
Index and detail display
Store the FK, resolve the label on read, and don't let the index table render a bare integer:
SelectPlus::make('Customer')
->displayUsing(fn ($value) => Customer::find($value)?->name)
->exceptOnForms(), // when paired with a separate form-only field
Or the inverse — ->onlyOnForms() on the picker and a plain Text on the index — which is usually cheaper than a per-row find().
Testing
SelectPlus inherits Nova's field assertions, so Nova::assertField... and standard Dusk form interactions work unchanged. For Ajax mode, the only thing worth asserting at the PHP level is your options endpoint (auth + filtering); the picker behaviour itself needs a Dusk test or a manual pass. Add one Dusk test per cascading field that selects a parent and asserts the child's options actually change — that's the regression 2.0.2 addressed, and it's cheap insurance for the next patch.
Maintenance risk is still the real cost. Low stars, effectively no dependents, and a long quiet period before this 2.0.2 patch. A fresh patch release is a good sign but not a cadence — pin an exact version in composer.json, and if you adopt it, budget for a fork. Nova 5's asset and field-internal changes remain the most likely breakage point — smoke-test both the form and the index view, not just composer install.
DependsOn before 2.0.2. PR #57 fixes DependsOn integration on the Form.vue side, which means form-side dependency wiring was genuinely broken/unreliable in 2.0.0–2.0.1. If you're on those, upgrade before debugging your own code. Confirm the built assets that ship with 2.0.2 are the ones Nova serves — after upgrading, re-publish/clear caches (php artisan nova:publish, php artisan route:clear) so you aren't testing stale JS against new PHP.
Form styling changed in 2.0.2. PR #56 touched the field's form styles. Cosmetic, but if you've overridden classes or shipped custom CSS targeting the field markup, re-check field height/alignment in your form layouts after upgrading.
Numeric option keys. PHP arrays with integer-ish keys (Category::pluck('name','id')) JSON-encode as objects sometimes and arrays other times depending on ordering. If labels come out blank or values arrive as floats, cast keys to strings before handing them to ->options():
->options($options->mapWithKeys(fn ($label, $id) => [(string) $id => $label])->all())
Empty selection vs. missing key. A null value with ->rules('required') fails server-side but the picker may look filled if a default sneaks in. Pin it down with ->default(null)->nullable() and test the "clear then save" flow explicitly — clearing a searchable select is a different code path than selecting.
Depends-on ordering. The child field's resolver reads parent values from the current form state, so the parent must be present in the same form (not an inline-relation or a different tab that hasn't mounted). Still clear the child yourself when the parent changes — the package reloads options but does not reliably invalidate a stale child value, and a stale value will silently save a mismatched pair. Watch the network tab on a parent change; on ≥2.0.2 you should see a request. If you see a request carrying the old parent value, you're off by one on the change event; if you see none on an older version, upgrade first before assuming misconfiguration.
Ajax debugging. The two failure modes are 401/403 (endpoint not in Nova's middleware group, or a nova route cache issue after deploy) and 200-with-wrong-shape (silent empty dropdown, no console error). Check the response body before you check the Vue code. After deploying, php artisan route:clear and php artisan nova:publish are the usual suspects.
Don't use it for relationships. If the column is a real belongsTo, Nova's BelongsTo gives you searchable-by-default plus create/edit links and correct authorization. SelectPlus is for raw scalar columns and enum-ish lookups.
N+1 on the index. A displayUsing(fn ($v) => Model::find($v)->name) runs once per row. Swap in a cached map, or gate the picker to forms and show a BelongsTo-backed text column on the index.
Extension points. You can subclass SelectPlus, override component() to point at your own Vue component, and register it with Nova::script(). That's the clean path if you need tagging, multi-select with Ajax, or custom option rendering — fork the Vue component rather than patching node_modules-style, since the package has no release cadence you can rely on to carry a patch forward.
How can I help you explore Laravel packages today?