sanderbaas/laravel-nova-belongsto-dependency
A Laravel Nova field that makes BelongsTo selects depend on another field's value, enabling cascading dropdowns in Nova resources. Pick a parent option and the dependent BelongsTo field updates its available options to match.
Install
composer require sanderbaas/laravel-nova-belongsto-dependency
Because this is a Nova package, the parent laravel/nova repository must already be authenticated in composer.json. Check the package's composer.json for the Nova version constraint before installing on a Nova 4/5 app — the constraint is the first thing that will tell you whether this is even usable in your project.
Where to look first
README.md — the usage snippet is the whole contract; there is very little else.src/BelongsToDependency.php — you want to see exactly which methods it adds on top of Laravel\Nova\Fields\BelongsTo (the dependsOn() signature is the one detail worth confirming rather than guessing).resources/js/field.js (or dist/js/field.js) — this is where the "dependency" actually lives. It watches a sibling field and appends its value to the associatable search request. Read it once; it's small.dist/js/ — the compiled asset that Nova actually loads. If dist/ is committed, you don't need to run npm. If it isn't, you do.First use case — cascading Country → City
use Laravel\Nova\Fields\BelongsTo;
use SanderBaas\BelongsToDependency\BelongsToDependency;
public function fields(Request $request)
{
return [
BelongsTo::make('Country', 'country', Country::class)
->searchable(),
BelongsToDependency::make('City', 'city', City::class)
->searchable()
->dependsOn('country', function ($query, $countryId) {
return $query->where('country_id', $countryId);
}),
];
}
The closure receives the relateable query for City and the current value of the watched field; return the constrained query. The JS component takes care of re-firing the search endpoint when the watched field changes.
Single dependency (the common case). Watch one sibling field, constrain the related model's query. Keep the constraint in a local scope on the related model so the same logic is testable outside Nova:
->dependsOn('country', fn ($query, $id) => $query->forCountry($id))
Multiple dependencies. Chain dependsOn() calls, or have the closure read several keys off the request. If the package only passes one value, fall back to the request:
->dependsOn('country', function ($query, $countryId) {
$query->where('country_id', $countryId);
if ($regionId = request()->input('region')) {
$query->where('region_id', $regionId);
}
return $query;
})
Fail open on empty. On create forms the parent field is null on first render. Return the query untouched when the value is empty rather than where('country_id', null) — an unconstrained list is friendlier than an empty one, and Nova will still filter once the user picks a country.
Pair it with relatable() for hard limits. relatable() is evaluated server-side every request and is a good place to put authorization/tenant constraints; dependsOn() is for the dynamic, user-driven part. Don't duplicate the two.
Reusable field factory. If several resources share the same cascade, extract it:
public static function cityField(): BelongsToDependency
{
return BelongsToDependency::make('City', 'city', City::class)
->searchable()
->dependsOn('country', fn ($q, $id) => $q->forCountry($id));
}
Index and detail views. The dependency is a form behaviour. On index/detail, the field renders like a normal BelongsTo — no filtering, no extra request. Don't expect it to constrain anything in filters or lenses.
Testing. The behaviour ultimately lives in one query closure. Write a feature test that hits Nova's associatable endpoint and asserts the returned resources:
$this->actingAs($user)
->getJson("/nova-api/cities/associatable/city?country={$country->id}")
->assertJsonFragment(['id' => $city->id]);
Also unit-test the closure directly by invoking it with a query builder — it's plain Eloquent, no Nova needed.
Inline creation. If you enable showCreateRelationButton(), be aware the created record won't automatically inherit the parent's value — the modal doesn't know about the dependency. Either hide inline creation on that field or pre-fill the foreign key in your model's creating event.
searchable() is mandatory. The dependency piggybacks on Nova's /nova-api/.../associatable/... search request. A non-searchable BelongsTo is a plain select and the package has nothing to hook into.
Field names must match exactly. The JS watches the sibling field by its request attribute (e.g. country, or country_id depending on how the field is named in fields()). A silent mismatch is the #1 cause of "it does nothing" — verify in the Network tab that the associatable request carries your dependency value.
Stale dependent values. Changing the country does not clear an already-selected city. If that matters, add a small custom JS listener (Nova.$on('field-changed')) to null out the dependent field, or accept the stale value and validate server-side.
Server-side validation is your job. Nothing enforces that the saved city belongs to the selected country. Add:
Rule::exists('cities', 'id')->where('country_id', $request->country)
This is also the only defence if the field is submitted by API clients rather than the Nova UI.
Authorization is inherited. The associatable endpoint respects viewAny on the related model. If a user legitimately can't search a relation, they'll see "no results", not an error — check policies before blaming the package.
Query cost. The closure runs on every keystroke-driven search request. Make sure the constrained columns are indexed, and avoid whereHas chains that turn a typeahead into a slow query.
Version/asset compatibility. This package predates Nova 4/5 asset handling in all likelihood. If the JS component fails to register, check nova:publish/vendor:publish tags, confirm dist/js/field.js exists, and verify the component name matches what Nova::script()/the service provider registers. Nova major-version mismatches usually surface as a blank field, not an exception.
Debugging path. Browser Network tab → find the associatable request → confirm the dependency query param is present and correct. If it is, the problem is in your closure; if not, the problem is in the JS/field-name binding. DB::listen() or Telescope will show you the generated SQL.
Abandonment risk. Zero stars, zero dependents, no recent activity. Treat it as a small piece of vendored code: pin the exact version, and be prepared to fork. The escape hatch is straightforward — a custom field extending BelongsTo that overrides the relateable query using request() input, plus ~20 lines of Vue to append the dependency value to the search request. Budget half a day for that fallback if the package breaks on your Nova version.
Extension points. Subclass BelongsToDependency and override the query-building method for cross-cutting behaviour; register additional JS with Nova::script() instead of editing vendor/. Never edit files under vendor/ — the next composer update erases them.
How can I help you explore Laravel packages today?