emilianotisato/nova-belongsto-depends
A Laravel Nova field that makes BelongsTo selects depend on another form field: options reload dynamically based on the parent value, so related records stay filtered as users change selections in resource forms.
Install the field with Composer and let Nova's package discovery pick it up:
composer require emilianotisato/nova-belongsto-depends
There is no migration or config file to publish in the typical Nova-field layout. If the package ships a service provider that registers assets, run php artisan vendor:publish --provider="Emilianotisato\NovaBelongsToDepends\FieldServiceProvider" (or nova:publish on newer Nova versions) and check that dist/js/field.js is registered via Nova::script().
The public API is intentionally small, so verify the exact signature in src/BelongsToDepends.php before wiring it in. The intended shape is a BelongsTo field with a dependsOn() modifier that names the sibling field to watch plus a way to resolve the filtered options (a URL/endpoint or a callback evaluated server-side):
use Emilianotisato\NovaBelongsToDepends\BelongsToDepends;
BelongsToDepends::make('City', 'city_id')
->dependsOn('state_id', '/nova-vendor/cities')
->searchable();
Where to look first, in order:
README.md — the only authoritative source for the dependsOn() signature and expected endpoint contract.src/BelongsToDepends.php — the Nova field class; everything it exposes lands in withMeta(), which the browser component reads.dist/js/field.js (or resources/js/components/FormField.vue if source is included) — this is where the "watch parent field → refetch options" behavior actually lives. Read it before assuming how re-renders behave.composer.json — check the laravel/nova constraint; a Nova 2/3-era field will silently break under Nova 4/5's asset pipeline.First use case: a two-level cascade on a Resource form — Category → Product, State → City, Country → Region — where the child dropdown must reset and re-query whenever the parent changes. This is the single thing the package exists for; start there before trying anything more elaborate.
Basic cascade. Put the parent BelongsTo and the dependent field in the same fields() panel. The dependent field needs a resolvable options source — either a Nova-vendor route you own or the package's built-in associatable endpoint with an added filter parameter. Keep the parent field nullable() if the child should be optional, and pair it with ->rules('required') on the child only when a parent is guaranteed.
Filtering options server-side. Do not try to push the full option set into the browser and filter in Vue. For anything beyond a few dozen rows, expose a small route under /nova-vendor/... protected by Nova's middleware (nova guard) that accepts the parent ID and returns {label, value} pairs. The field component should hit that route through Nova.request() so auth, CSRF, and error handling are inherited automatically.
Reusable field builders. Because the API is thin, wrap it in a helper so every resource uses the same shape:
class DependentSelect
{
public static function make(string $name, string $parent, string $endpoint, ?string $label = null)
{
return BelongsToDepends::make($label ?? $name, $name)
->dependsOn($parent, $endpoint)
->searchable()
->nullable();
}
}
This keeps the dependsOn contract in one place when you eventually need to swap or extend the field.
Clearing stale values. The most common production bug is the child retaining its old ID after the parent changes. Two fixes: (a) have the Vue component emit null to the form field when the parent watcher fires (check FormField.vue for a handleChange hook), and (b) defend on the server with a Rule::exists scoped to the parent, e.g. Rule::exists('cities', 'id')->where('state_id', $request->state_id).
Searchable + dependent. Nova's searchable() hits the associatable endpoint independently of your dependency filter. If you enable both, make sure the search endpoint applies the same scoping — otherwise search returns rows the cascade would have hidden.
Detail and index pages. The dependency only affects the form. On index/detail the field renders as a normal BelongsTo label, so don't expect the same filtering in read views; it's display-only.
Inline creation and modals. Nova's "create related" modal does not know about your cascade. If the field allows inline creation, hide it (->hideCreateRelationButton()) rather than ship a modal that ignores the parent selection.
app/Nova/Fields/ or vendor it, pin the commit, and read FormField.vue end to end. The assessment score (~1.05) reflects a package that has not been battle-tested across Nova versions or edge cases.dist/ freshness and the laravel/nova constraint in composer.json; if it's ^2.0 or ^3.0, expect to recompile the JS yourself with the Nova-matching toolchain.Rule::exists(...)->where(...) in the FormRequest or rules().dependsOn('state_id') only works if state_id is rendered in the same form and is visible. Fields hidden via canSee() or moved into a separate panel with conditional display won't trigger reliably. Confirm the event the Vue component listens to ($watch, Nova's field-changed bus, or a Nova.$emit) and that your layout preserves it.BelongsTo inline-create, the child endpoint may not yet include the new parent's children on the next tick. Force a refresh by keying the watcher on the parent value, not on mount.meta in the /nova-api/{resource} schema response to confirm dependsOn serialized; (3) temporarily log inside the Vue watcher to verify the value changes; (4) check the browser console for Nova is not defined — a sign the asset wasn't registered via Nova::script().nova middleware group, otherwise the fetch returns the login redirect and the dropdown silently empties. This looks like a dependency bug but is almost always routing.Cache::remember('cities.'.$stateId, 3600, ...)) and invalidate on save via a model observer. Do not load every possible option on mount.BelongsToMany / MorphTo. The field is named BelongsTo for a reason; don't expect polymorphic or pivot-based relations to work. For those, write a custom field or fall back to a filtered relatable() query with a full form reload.$component = 'your-dependent-select'), copying the existing component and adding the hooks you need (clear-on-change, debounce, caching headers). That keeps you on the package's API surface while owning the parts most likely to need changes.How can I help you explore Laravel packages today?