royduin/laravel-nova-field-dynamic-select
royduin/laravel-nova-field-dynamic-select adds dynamic select fields to Laravel Nova. Options can change based on another field’s value, making dependent, cascading dropdowns easier to build without custom JavaScript. Useful for state/city or category/subcategory forms.
composer require royduin/laravel-nova-field-dynamic-select
No service provider registration, no config file, no vendor:publish. The field ships pre-compiled JS/CSS, so it works immediately after composer require (the Laravel Mix 6 bump in 1.10.0 only affects people rebuilding assets — see Gotchas).
Where to look first, in this order:
src/DynamicSelect.php — every method you'll call (dependsOn, options, the resource helper, color helpers) lives here. When the README is ambiguous, this class is the source of truth.resources/js/ — the Vue component, only if you need to know exactly when the dependent request fires (useful when debugging stale options).First use case — a Country → City pair on a resource form:
use Royduin\DynamicSelect\DynamicSelect;
public function fields(Request $request): array
{
return [
DynamicSelect::make('Country')
->options(Country::pluck('name', 'id')),
DynamicSelect::make('City')
->dependsOn('Country')
->options(fn (Request $request) => $request->filled('country')
? City::where('country_id', $request->input('country'))->pluck('name', 'id')
: []),
];
}
Load the form, change Country, and confirm City repopulates. If that works, you understand the package — everything else is variation on this loop.
Closure-backed options read the current request. The options() closure runs server-side on each dependency change, so you can query Eloquent directly. Prefer $request->input('country') over request()->all() so the closure stays testable and explicit about what it needs.
Three-level chains. A → B → C works if each level declares only its immediate parent. Keep the chain shallow; every hop is a round trip and a chance for a stale intermediate value.
Reusing a Nova resource's fields. The 1.10.0 change ("resource fields taken from the specific resource on update instead of empty generic one") targets the path where you point the field at a resource rather than supplying a raw array. If you maintain one resource definition and reuse it in a second form, this is the mode you want — and it is now correct on update forms, which it previously was not.
Share option resolution between the field and the validator. The single most common defect in dynamic selects is the field and the validation rule drifting apart. Extract the query into one place and call it from both:
class CityOptions
{
public static function for(?int $countryId): array
{
return City::where('country_id', $countryId)->pluck('name', 'id')->all();
}
}
DynamicSelect::make('City')
->dependsOn('Country')
->rules(['nullable', Rule::in(array_keys(CityOptions::for($request->input('country'))))])
->options(fn (Request $request) => CityOptions::for($request->input('country')));
Nullability and defaults. Always make the dependent field nullable (or sometimes) at the database level too — a user who fills the child before the parent, then clears the parent, will submit a combination your form no longer offers.
Custom colors (new in 1.10.0). Option colors are no longer locked to a fixed palette. This is worth adopting for status-like selects where the color is semantic rather than cosmetic — but pin the version in composer.json if the syntax is load-bearing in your UI, since it landed in a minor release and the exact API may still settle.
Update forms. From 1.10.0, options resolve against the specific resource being edited rather than a blank generic instance. If you have edit screens where a dependent select showed the wrong (or an empty) option set, upgrading without code changes is the fix.
Each dependency change is an HTTP round trip. Index the foreign key column the closure filters on, and keep the closure free of work you don't need on every keystroke. For expensive option sets, Cache::remember("cities.{$countryId}", 300, ...) inside the shared resolver.
Upgrading from ≤1.9.1 to 1.10.0 is a bug fix, not just a build bump. If you have workarounds in place for blank/generic option sets on update forms (hardcoding options in resolveUsing, duplicating fields per context, forcing a fresh instance), remove them and retest — they may now double-apply or mask the corrected behaviour.
Laravel Mix 6 is a maintainer/contributor concern, not a consumer one. Compiled assets are committed, so you don't need Node at all in a normal install. Only touch it if you are patching the JS: Mix 6 needs Node 14+, and running npm install inside vendor/ is a dead end — fork the package or use a path repository.
A stale child value after clearing the parent is the #1 bug. Nothing in the field resets the child's selected value for you. Combine a nullable rule, a Rule::in check, and an explicit reset in your form's saving/fill logic if the combination must never reach the database.
Validate on the server, always. The client-side option list is a convenience. Forged input bypasses it entirely; the Rule::in against the same resolver (above) is what actually protects you.
Field order matters. Declare the parent before the child in the fields() array. The component looks the parent up by attribute name in the rendered form, and a name typo in dependsOn() fails silently — you get an empty child, not an exception.
Searchable behaviour is client-side over loaded options. The search box filters only what the last request returned. If a parent has thousands of children, you need to cap the option set or rethink the interaction; search will not page in more.
Debugging recipe. Open the browser Network tab, change the parent, and inspect the dependent-field request and its JSON response — that is where wrong/empty options are visible. On the server, ray()/logger() inside the closure confirms whether it fired at all, which cleanly separates "field never re-queried" (front-end/wiring) from "closure returned nothing" (query).
Version pin. This is a small single-maintainer package. Lock the constraint (^1.10) and skim the diff on minor bumps — the 1.10.0 changelog mixes build tooling, a behavioural fix, and a new feature, which is a good sign that minor versions aren't strictly additive.
How can I help you explore Laravel packages today?