rodrigofs/filament-masterdetail

Filament plugin designed specifically for managing Laravel HasMany relationships without relying on Filament's default repeater field, using instead a dedicated modal and table-based form interface.

v1.3.0 2025-04-21 03:58 UTC

README

Filament Master Detail

Latest Version on Packagist PHP Run Tests fix-code-style phpstan Total Downloads

Overview

Filament Master Detail is a dynamic management plugin Form Component for HasMany (1,n) relationships in FilamentPHP. It allows you to add and remove related records directly within the parent form, without the need to save the parent record first. Ideal for fast and fluid data entry scenarios.

Table of Contents

Installation

Requirements

  • PHP >= 8.1
  • Laravel >= 10
  • Filament >= 3.x

Installation Steps

  composer require rodrigofs/filament-masterdetail

Basic Usage

Important: When using the table(...) method, it is not compatible with Filament's TextColumn or other default columns. You must exclusively use the DataColumn provided by this package.

Example

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;
use Rodrigofs\FilamentMasterDetail\Tables\Columns\DataColumn;

MasterDetail::make('items')
    ->relationship('items')
    ->schema([
        TextInput::make('name')->required(),
        TextInput::make('description'),
    ])
    ->table([
        DataColumn::make('name'),
        DataColumn::make('description'),
    ]);

Define the HasMany relationship in the parent model:

public function items(): HasMany
{
    return $this->hasMany(Item::class);
}

Common Use Cases

Order Creation with Items

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;
use Rodrigofs\FilamentMasterDetail\Tables\Columns\DataColumn;

MasterDetail::make('items')
    ->relationship()
    ->schema([
        Select::make('shop_product_id')
            ->label('Product')
            ->options(Product::query()->pluck('name', 'id'))
            ->required()
            ->reactive()
            ->afterStateUpdated(fn ($state, Set $set) => $set('price', Product::find($state)?->price ?? 0))
            ->distinct()
            ->disableOptionsWhenSelectedInSiblingRepeaterItems()
            ->columnSpan(['md' => 5])
            ->searchable(),

        TextInput::make('quantity')
            ->label('Quantity')
            ->numeric()
            ->default(1)
            ->required()
            ->columnSpan(['md' => 2]),

        TextInput::make('price')
            ->label('Unit Price')
            ->numeric()
            ->disabled()
            ->dehydrated()
            ->required()
            ->columnSpan(['md' => 3]),
    ])
    ->unique('shop_product_id')
    ->table([
        DataColumn::make('product.name')
            ->relationship()
            ->label('Product'),

        DataColumn::make('quantity')
            ->label('Quantity'),

        DataColumn::make('price')
            ->label('Unit Price'),

        DataColumn::make('total')
            ->formatStateUsing(fn ($rowLoop) => $rowLoop->price * $rowLoop->quantity)
            ->label('Total'),
    ]);

Additional Features

Behavior & Customization

You can customize the behavior of the component and the appearance and behavior of the modal used to add, delete, and edit related records:

Disable Actions

Disable the default Add, Delete, and Edit actions on the component:

use Rodrigofs\FilamentMasterDetail\Components\MasterDetail;

// Disable actions statically
MasterDetail::make('items')
    ->addable(false)
    ->editable(false)
    ->removable(false)
    ->schema([
        // Form fields
    ]);

// Disable actions conditionally using closures
MasterDetail::make('items')
    ->addable(fn (): bool => /* condition based on $record, $get, $state, $operation and more... */ false)
    ->editable(fn (...): bool => /* condition based on $record, $get, $state, $operation and more... */ false)
    ->removable(fn (...): bool => /* condition based on $record, $get, $state, $operation and more... */ false)
    ->schema([
        // Form fields
    ]);

Slideover Mode

Display the form inside a Slideover instead of a traditional modal:

use Rodrigofs\FilamentMasterDetail\Components\MasterDetail;

MasterDetail::make('items')
    ->slideover()
    ->schema([
        // Form fields
    ]);

Set Custom Labels

Define the labels for modal actions and headings:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->addActionLabel('Add Product')
    ->modalHeading('Add Product')
    ->modalDescription('Include a new product in this order.')
    ->modalSubmitActionLabel('Add')
    ->modalSubmitEditActionLabel('Edit')
    ->modalCancelActionLabel('Cancel');

Set Modal Icon and Width

Customize the modal icon and size:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->modalIcon('heroicon-o-plus')
    ->modalWidth('lg');

Keep Modal Open After Adding

Prevent the modal from closing automatically after adding a record:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->modalPersistent();

Customize Table Heading

Set a custom heading for the related records table:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->heading('Order Items');

Preserve Field Values

Prevent specific fields from being cleared after adding a record:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->formExceptClear(['product_id']);

Manipulate Data Before Adding

Allow data manipulation before the record is added to the table:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->beforeAddActionExecute(fn ($state, $set) => $set('product_id', $state));

Add Header Actions

Define custom actions in the header of the MasterDetail component:

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->headerActions([
       Action::make('reset')
            ->modalHeading('Are you sure?')
            ->modalDescription('All existing items will be removed from the order.')
            ->requiresConfirmation()
            ->color('danger')
            ->action(fn (Forms\Set $set) => $set('items', [])),
    ]);

There are many additional features available—more than can be covered at once. While I will continue to document them, I encourage you to freely explore all possibilities. Don’t hesitate to open an issue if you encounter any problems or have suggestions.

Full Example

use Rodrigofs\FilamentMasterDetail\Forms\Components\MasterDetail;

MasterDetail::make('items')
    ->relationship()
    ->schema([
        Select::make('product_id')
            ->label('Product')
            ->options(Product::query()->pluck('name', 'id'))
            ->required(),
        TextInput::make('quantity')
            ->numeric()
            ->required(),
    ])
    ->table([
        DataColumn::make('product.name')
            ->relationship()
            ->label('Product'),
        DataColumn::make('quantity')
            ->label('Quantity'),
 
    ])
    ->addActionLabel('Add Product')
    ->modalHeading('Add Product')
    ->modalDescription('Include a new product in this order.')
    ->modalIcon('heroicon-o-plus')
    ->modalWidth('lg')
    ->modalSubmitActionLabel('Add')
    ->modalCancelActionLabel('Cancel')
    ->heading('Order Items')
    ->formExceptClear(['product_id'])
    ->beforeAddActionExecute(fn ($state, $set) => $set('product_id', $state))
    ->headerActions([
        Action::make('reset')
            ->modalHeading('Are you sure?')
            ->modalDescription('All existing items will be removed from the order.')
            ->requiresConfirmation()
            ->color('danger')
            ->action(fn (Forms\Set $set) => $set('items', [])),
    ])
    ->slideOver();

Editing Behavior

The editing feature works for related records defined via the relationship() method on the component.

Important: This feature does not support custom statePath-based implementations that do not use the relationship() method. Relationship-based binding is required for the edit action to resolve the model instance correctly.

FAQ

  1. Do I need to save the parent record before adding related records? No. MasterDetail allows adding and removing related records before persisting the parent model.

  2. Does it support other relationship types besides HasMany? Currently, only HasMany relationships are supported.

  3. Is there support for editing related records? No. Only adding and removing records is supported at the moment. Yes. You can edit related records through an edit action.

Screenshots

Table View

Table View

Add New Item in Modal

Add New Item

Edit Item in Modal

Edit Item

Remove Item with Confirmation

Remove Item

Show Slideover Mode

Slideover Mode

Video Demo

Video Demo

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.