Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Parental Field Laravel Package

wamadev/nova-parental-field

wamadev/nova-parental-field provides a Laravel Nova field for selecting a parent record, making it easy to manage hierarchical or self-referencing relationships directly from Nova forms. Simple to install and configure, it helps organize nested content and tree-like data without custom UI code.

View on GitHub
Deep Wiki
Context7

Getting Started

Precondition check before install. Nova is a paid, private package; Composer needs credentials for nova.laravel.com in auth.json or the install will fail for reasons unrelated to this field. Confirm the Nova major version your app is pinned to (3.x vs 4.x) — a field released in mid-2021 was almost certainly built against Nova 3.

composer require wamadev/nova-parental-field
# pin exactly, do not float — see Gotchas

Where to look first: the vendor directory, not the docs. There is no README, no changelog, and the release body at v0.0.0 is empty. Treat the source as the specification. Read, in order:

  1. vendor/wamadev/nova-parental-field/composer.json — the illuminate/* and laravel/nova constraints, plus extra.laravel.providers (whether a provider is auto-discovered).
  2. src/ — the field class. Note its parent class (Field, Select, or BelongsTo), its $component string, and its public setters. Those setters are the API.
  3. resources/js/field.js and dist/js/field.js — the Vue component. The props block tells you exactly which option shapes the frontend accepts. If dist/ is empty or absent, the field will render a blank component and you'll spend an hour blaming your resource.
  4. package.json — the laravel-nova npm version, which pins the Nova major it was compiled against.

Data model. The field only makes sense with a self-referential schema, so set that up first:

Schema::table('categories', function (Blueprint $table) {
    $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
});
class Category extends Model
{
    public function parent(): BelongsTo
    {
        return $this->belongsTo(self::class, 'parent_id');
    }

    public function children(): HasMany
    {
        return $this->hasMany(self::class, 'parent_id');
    }
}

First use case. A Category resource where an editor picks a parent from existing categories:

use Wamadev\NovaParentalField\NovaParentalField;

public function fields(NovaRequest $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Name')->rules('required', 'max:255'),
        NovaParentalField::make('Parent', 'parent_id')->nullable(),
    ];
}

Verify the class name and namespace against src/ before trusting that import — at 0.0.0 the namespace is not guaranteed to match the package name.

The baseline you must compare against. Nova already ships a self-referential parent selector:

BelongsTo::make('Parent', 'parent', Category::class)
    ->nullable()
    ->searchable(),

If your hierarchy is large, searchable() on BelongsTo is almost certainly the better day-to-day choice, because it queries by search term instead of rendering every row into an options array. Install this package only if its component offers something that BelongsTo does not — and confirm that by reading field.js first. If it doesn't, stop here; you have your answer in ten minutes rather than a sprint.

Implementation Patterns

Flat options, small hierarchies. If the package exposes an options()-style setter (read src/ to confirm), keep it bounded. Rendering several thousand <option> elements will stall the form:

NovaParentalField::make('Parent', 'parent_id')
    ->options(Category::orderBy('name')->pluck('name', 'id'))
    ->nullable(),

Exclude the record itself and its descendants. The single most important guard: a node must never be its own parent, and picking a descendant creates a cycle that will infinite-loop any tree renderer you write later.

->options(function (NovaRequest $request) {
    $excluded = [$request->resourceId]; // on update; null on create
    if ($request->resourceId) {
        $excluded = array_merge(
            $excluded,
            $this->descendantIds((int) $request->resourceId)
        );
    }
    return Category::whereNotIn('id', array_filter($excluded))
        ->orderBy('name')
        ->pluck('name', 'id');
})

Pair it with a server-side rule so a crafted request can't bypass the UI:

->rules('nullable', 'exists:categories,id', new NotADescendant($this->resource))

Scope the options to the tenant. Multi-tenant apps must filter the option list, otherwise the selector leaks the names of other tenants' records:

->options(Category::where('tenant_id', $request->user()->tenant_id)->pluck('name', 'id'))

Show the parent on index without the field. Keep the form-only field off index() and use the relation for the column — cheaper and it avoids whatever the component does when rendered in a table cell:

BelongsTo::make('Parent', 'parent', Category::class)
    ->exceptOnForms()
    ->nullable(),

Then eager load to avoid N+1: public $with = ['parent']; on the resource.

Only where it belongs in the lifecycle. On create, a record often has no meaningful parent yet (or becomes its own root). Guard the form:

NovaParentalField::make('Parent', 'parent_id')
    ->nullable()
    ->hideWhenCreating(),

Wrapper from day one. At 0.0.x the upstream API is untrusted. Bind the package behind your own class so a breaking patch bump is one file, not a codebase sweep:

final class ParentSelect
{
    public static function make(string $name = 'Parent', string $attribute = 'parent_id'): Field
    {
        return NovaParentalField::make($name, $attribute)
            ->nullable();
    }
}

Keep the return type as Nova's Field contract, never the package type, so removal is mechanical.

Testing. Nova resources are testable without a browser; assert the field appears and resolves correctly rather than trusting manual clicks:

$this->actingAs($admin)
    ->get("/nova-api/categories/{$category->id}")
    ->assertOk();

Add a unit test on the descendant filter itself — that's where the real logic lives, and it's the part you own.

Gotchas and Tips

No release notes at 0.0.0 — pin exactly. Use "wamadev/nova-parental-field": "0.0.0" with a committed composer.lock. Never ^0.0 or *. Configure Renovate/Dependabot to open a PR — with mandatory human review — for any 0.x bump.

Nova 4 will likely break the component. A field compiled in 2021 targets Nova 3's Vue component contract (props, fieldValue, the FormField mixin). Symptoms on Nova 4: the select renders empty, the value never persists, or the whole form panel loses styling. Before rollout, open one create and one update form and confirm the selected parent survives a save and a page reload. If it doesn't, the honest conclusion is that you need ~40 lines extending Laravel\Nova\Fields\Select yourself.

dist/js/field.js must exist. Some low-traffic Nova packages ship without a built asset and expect you to run npm install && npm run prod inside vendor/ — which is not reproducible in CI and will be wiped by any composer install. If dist/ is missing, that is a hard stop: fork it and build it into your own asset pipeline.

Cycle creation is your problem, not the package's. A parent selector will happily let a user set A → B → A. Guard on both sides (options filtering plus a validation rule) and add a isDescendantOf() helper covered by tests. Also beware nullOnDelete versus cascadeOnDelete on the FK — cascading a delete through a self-referential tree is a foot-gun; nullOnDelete promotes orphans to roots, which is usually more recoverable.

N+1 on index pages. Any field or column that displays the parent's name costs a query per row unless the relation is eager loaded. Watch the query count in Telescope on a 50-row index.

Attribute naming assumptions. The field may hard-code parent_id or a parent relation. If your schema uses parent_uuid or ancestor_id, you may be renaming columns to fit a UI widget — a bad trade. Confirm from src/ before adapting your schema.

Debugging the resolution path. When the select shows nothing, instrument the PHP side before touching Vue:

NovaParentalField::make('Parent', 'parent_id')
    ->resolveUsing(function ($value, $resource) {
        logger(['value' => $value, 'resource' => $resource->id]);
        return $value;
    })

If that logs correctly, the fault is in the JSON payload or the component; if not, it's the attribute name. dd() on the field's jsonSerialize() output also tells you which option shape the frontend expects.

Extension point: your own field class. The genuinely durable option here is to copy the pattern rather than the dependency:

class ParentSelect extends Select
{
    public function __construct($name, $attribute = 'parent_id', $resource = null, callable $resolveOptions = null)
    {
        parent::__construct($name, $attribute, $resolveOptions);
        $this->nullable();
    }
}

That's roughly the entire value proposition of a field this thin, it has no supply chain, and it inherits Nova's own version guarantees. At 0.0.0 with no changelog and no documented API, budget for owning the fork risk; the wrapper above is the exit door, and you should keep it free of package-specific types.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core