alvinhu/nova-child-select
Laravel Nova field that adds cascading, dependent select dropdowns to your forms. Child options update dynamically based on a parent field's selection, so you can build linked selects (like country then city) with a simple fluent API and no custom JavaScript.
Check compatibility before anything else. The package is archived, so treat it as frozen: open composer.json in the repo and confirm the illuminate/* / laravel/nova constraints against your app. It is a Nova 2/3-era field (Vue 2 component, pre-Inertia). On Nova 4+ it will not render — the front-end contract changed to Vue 3 + Inertia, and nothing in an archived repo will be updated for it. If you're on Nova 4, stop here and use the built-in dependsOn() plus server-resolved options() on a normal Select.
Install (Nova 2/3):
composer require alvinhu/nova-child-select
The service provider registers via auto-discovery. If you're on an older Laravel, add Alvinhu\ChildSelect\FieldServiceProvider::class to config/app.php.
Where to look first, in this order:
README.md — the whole feature surface is one code sample.src/ChildSelect.php — the field class. This tells you the exact method names (->parent(...), ->options(...)), the $component string, and how options are serialized into meta.resources/js/components/FormField.vue — this is where the "child" behavior actually lives: it watches the parent field's value and filters the nested option map.resources/js/field.js — component registration, useful if you decide to fork.First use case — country → city on a resource form:
use Laravel\Nova\Fields\Select;
use Alvinhu\ChildSelect\ChildSelect;
public function fields(Request $request)
{
return [
Select::make('Country', 'country')->options([
'cn' => 'China',
'us' => 'United States',
]),
ChildSelect::make('City', 'city')
->parent('country')
->options([
'cn' => ['bj' => 'Beijing', 'sh' => 'Shanghai'],
'us' => ['nyc' => 'New York', 'sf' => 'San Francisco'],
]),
];
}
The outer keys of options() are parent values, the inner arrays are the child choices. The stored value is the inner key.
Build the nested map from Eloquent, and cache it. The map is serialized into the field meta on every form resolution, so don't run N+1 queries per request:
ChildSelect::make('City')
->parent('country')
->options(Cache::rememberForever('cities_by_country', function () {
return City::query()
->orderBy('name')
->get(['country_code', 'code', 'name'])
->groupBy('country_code')
->map(fn ($cities) => $cities->pluck('name', 'code'))
->toArray();
})),
Keys must be the stored value of the parent field as a string — not the label, not the model ID unless the parent stores the ID.
Chaining for three levels (country → state → city). Each ChildSelect is itself a parent for the next one, so just point at the previous field's attribute:
Select::make('Country', 'country')->options($countries),
ChildSelect::make('State', 'state')->parent('country')->options($statesByCountry),
ChildSelect::make('City', 'city')->parent('state')->options($citiesByState),
This works because each field is just another Nova field with an attribute name; the component watches by attribute.
Scope to forms only. On index/detail the field usually falls back to rendering the raw stored key, which reads badly. Either hide it (->onlyOnForms()) or add a companion display field:
Text::make('City', function () {
return $this->city ? City::find($this->city)?->name : null;
})->onlyOnDetail(),
Always re-validate server-side. The client-side filtering is presentation only; a crafted request can post any child value. Add a scoped rule:
ChildSelect::make('City', 'city')
->parent('country')
->options($citiesByCountry)
->rules(['required', function ($attribute, $value, $fail) use ($request) {
$allowed = array_keys($citiesByCountry[$request->input('country')] ?? []);
if (! in_array($value, $allowed, true)) {
$fail('The selected city does not belong to the selected country.');
}
}]),
Know what dependsOn() does and doesn't do. Nova's built-in dependsOn() shows/hides fields based on another field's value — it does not filter a select's options. That gap is exactly what this package fills. If you only need conditional visibility, skip the package entirely and avoid the archived-code risk.
'1' => ... becomes 1 => ...), they arrive in JS as object keys (strings), and any strict comparison in FormField.vue between the parent value and the map key can silently fail. Symptom: the child is always empty for some parents but fine for others. Fix by prefixing keys ('c_1') or casting parent values to strings consistently.->parent('country') must match the parent field's attribute, in snake_case. ->parent('Country') will resolve nothing and fail silently.saving() when the parent is dirty.->default(), confirm the child populates on first render; if not, set the child's default to a value valid for that parent, or leave it null and rely on ->rules('required').HasMany, BelongsTo searchable sub-forms, or any independent form scope do not share reactive state with the top-level form. A ChildSelect nested in another field's sub-form will not see the parent — it will just show an empty or full option list.json_encode($field) in a tinker/test to confirm options and parent are what you expect; (2) open the Nova form request in the network tab and check the field's meta; (3) only then open FormField.vue. Ninety percent of failures are in step 1.php artisan nova:field acme/child-select) and copy FormField.vue as a starting point — that also lets you add async loading and a proper reset."alvinhu/nova-child-select": "1.0.2" or whatever you install) and add a note in your upgrade checklist to replace it before any Nova major upgrade.How can I help you explore Laravel packages today?