njxqlus/filament-relation-manager-component
Use Filament Relation Managers anywhere in your app, not just on resource pages. Drop them into forms, schema components, or tabs with a simple RelationManager component, control lazy loading, and optionally publish the package views.
Install
composer require njxqlus/filament-relation-manager-component
# only if you intend to override the blade markup
php artisan vendor:publish --tag="filament-relation-manager-component-views"
Choose the right major version first. This is the single most important setup decision, because the two branches target different Filament majors:
| Package | Filament | Laravel |
|---|---|---|
^1.1 |
v3 | up to 11/12 (see 1.1.3) |
^2.2 |
v4 / v5 | up to 13 (2.2.2) |
Check composer.json constraints on the tag you install rather than trusting the README tables — the 1.1.3 and 2.2.2 changelogs both advertise "Laravel 13 compatibility", so version-to-framework mapping is ambiguous from release notes alone.
Where to look first
README.md → the two usage blocks (form + infolist).src/Components/Forms/RelationManager.php and src/Components/Infolists/RelationManager.php — the whole public API is make(), manager(class-string), lazy(bool), plus everything inherited from Schema\Component (visible(), columnSpanFull(), etc.).resources/views/ after publishing — this is the only real customization surface.First use case: an Edit page where related records currently live in a separate Relation Manager tab that is hard to reach. Move it inline:
use Njxqlus\Filament\Components\Forms\RelationManager;
public static function form(Schema $schema): Schema
{
return $schema->schema([
Tabs::make()->tabs([
Tabs\Tab::make('Versions')->schema([
RelationManager::make()
->manager(RelationManagers\VersionsRelationManager::class)
->lazy(false),
]),
]),
]);
}
The manager class is used unmodified — no traits, no base class, no attribute. If it already works on the resource page, it works here.
The canonical pattern: one tab per relation, lazy(false) on the tab you want warm, lazy(true) (default) on the rest.
Tabs::make()->persistTabInQueryString()->tabs([
Tabs\Tab::make('Stands')
->schema([RelationManager::make()->manager(StandsRelationManager::class)->lazy(false)]),
Tabs\Tab::make('Contexts')
->schema([RelationManager::make()->manager(ContextsRelationManager::class)->lazy()]),
]);
Use lazy(false) only for the tab the user lands on. Everything else deferred — a resource with four embedded tables otherwise fires four paginated queries plus four header/summary queries on every page load.
use Njxqlus\Filament\Components\Forms\RelationManager as FormRelationManager;
use Njxqlus\Filament\Components\Infolists\RelationManager as InfolistRelationManager;
The Infolist variant is the one you want on ViewRecord pages: it renders the table read-mostly and is the correct choice when the surrounding schema is an Infolists\Infolist. The 1.1.1 release was specifically a fix for "displaying relation manager in infolist", so infolist embedding is the younger code path — smoke-test it on your Filament minor.
RelationManager collides with Filament\Resources\RelationManagers\RelationManager (the base class your managers extend) and with the resource's own namespace. Aliasing (as FormRelationManager) is not cosmetic here — it prevents accidentally passing the component class where a manager class-string is expected.
Since the component takes a plain class-string, you can drive it from a static map on the resource and keep the form/infolist definitions in sync:
protected static function embeddedManagers(): array
{
return [
'Versions' => RelationManagers\VersionsRelationManager::class,
'Stands' => RelationManagers\StandsRelationManager::class,
];
}
Build the Tab::make($label) loop from that map in both form() and infolist().
Before this package, inline editing of relations meant either a custom Filament page with a hand-rolled table, or a RelationManager exposed via getRelations() on a page you had to build. The component removes that: getRelations() stays empty, and the table lives where the user already is. Migrate those custom pages when you touch them; don't rewrite them proactively.
The embedded manager inherits the manager's own canViewForRecord() / can* policies, evaluated against the owner record resolved by the component. That means embedding does not bypass policy checks — but it also means a manager whose canViewForRecord() you tightened for the resource page behaves identically in a form. Verify with a user who lacks the relation permission before shipping, and layer ->visible() on the component if you want the tab to disappear rather than render an empty table.
The owner record must exist. This is the failure mode you will hit first. Embedding in form() on a Create page means there is no $record, so the manager's getOwnerRecord() cannot be satisfied. Restrict embedding to EditRecord, ViewRecord, and infolist contexts — or guard with ->visible(fn ($record) => $record !== null). Symptom is usually a typed-property "must not be accessed before initialization" error or an empty table, not a friendly exception.
Pin ^2.1 or later if you use modals/slideovers. 2.1.0 is literally "Fix ownerRecord assignment to work with modal/slideover". On 2.0.x and below, embedding inside a modal form leaves the owner record unset. If a tabbed relation manager works on a full page but dies in a ->modal() action form, this is why.
Lazy is on by default. The README passes ->lazy(false) on every example, which reads as boilerplate but is required — the component defaults to lazy. A lazily embedded table inside a tab that Filament never activates will simply never render, and inside a modal the placeholder may not resolve at all. When in doubt, lazy(false) first, optimize later.
Don't double-register. If the same manager class appears in getRelations() on the page and as an embedded component, you get two Livewire-mounted tables, duplicate action handlers, and confusing notification attribution. Pick one location per manager.
README namespaces are inconsistent. The infolist example mixes Schemas\Components\Tabs (outer) with Schemas\Infolists\Components\Tabs\Tab (inner tab). On Filament v3 the namespaces are entirely different (Filament\Forms\Components\Tabs) and Schemas\* does not exist. Copy the structure, then import from your installed version's actual class list — php artisan filament:about or IDE autocomplete, not the README.
Nested tables are expensive. Each embedded manager is a full table: query, pagination, filters, header actions, bulk actions. Three lazily-loaded tabs are fine; three non-lazy tabs plus a large form on a slow connection produce a visible stall, because everything hydrates in one Livewire request. Profile with the query log before blaming the package.
Publishable views are the extension point — and the trap. php artisan vendor:publish --tag="filament-relation-manager-component-views" copies the markup into your app, giving you full control over wrapper, spacing, and the loading placeholder. But published views are snapshot copies: a future package release will not update them, and if the package changes its view variables your copy silently breaks. Publish only when you actually need to change markup, and diff against vendor/njxqlus/.../resources/views after every upgrade.
Small-package risk. 46 stars, 0 dependents on Packagist, and a release history dominated by dependency bumps and Filament-major catch-up PRs. It is a thin wrapper, not a framework — but pin it in composer.json, read the diff of every minor bump, and keep the migration path in mind: the underlying capability is a handful of lines of view rendering around RelationManager::make(), so if the package stalls you can vendor the two component classes into app/Filament/Components/ and drop the dependency without touching a single manager class. That reversibility is the strongest argument for adopting it.
Debugging checklist when a table renders blank:
canViewForRecord() returning true for this user?getRelations()? (double mount)->lazy(false) applied as the last call on the component, not before ->manager() — chaining order is fine for these two methods, but mixing them into a conditional ->when() chain is where it silently drops.Testing. Because the manager is unmodified, existing Relation Manager tests keep passing — the embedded component is a rendering concern only. Cover it with a Livewire test that assertSees a known related record on the Edit page, and one negative test on a Create page asserting the tab is absent. That pair catches the owner-record and double-registration regressions, which are the only two failure modes this package realistically introduces.
How can I help you explore Laravel packages today?