yousefaman/filament-modal-repeater
Filament Modal Repeater displays repeater items in a compact table with modal-based editing, ideal for forms with many fields. Supports boolean, badge, money, and custom-formatted columns. Requires PHP 8.2+ and Filament v4 or v5.
Version gate first, before anything else. This package requires PHP 8.2+ and Filament v4 or v5 — there is no v3 path and no fallback. If you are on Filament 4.14+ or 5.8+, pin >=1.1.3. On earlier releases, ModalRepeater cannot even load, because Filament widened Component::schema() to accept Filament\Schemas\Schema and the package's override did not match the signature — you get a fatal error and a blank page, not a helpful exception.
composer require yousefaman/filament-modal-repeater
php artisan modal-repeater:install
Then optionally register the plugin in your panel provider (the README recommends it; the component itself works without it):
use YousefAman\ModalRepeater\ModalRepeaterPlugin;
public function panel(Panel $panel): Panel
{
return $panel->plugin(ModalRepeaterPlugin::make());
}
Where to look first. The README's usage block is effectively the entire public API: ModalRepeater plus Column. There is no docs site, no API reference, no examples repo — when the README runs out, read src/ directly. Read the changelog before upgrading anything: v1.1.1 (relationship binding, dot-notation columns, pivot LogicException) and v1.1.3 (the Filament signature break) tell you exactly where the sharp edges are. v1.1.2 is a no-op for behaviour — archive slimming only.
First use case. Do not adopt this for a two-field repeater; the modal is pure overhead there. Pick a form where each item carries 5+ editable fields and users mostly scan the list: order line items, price tiers, product variants, FAQ blocks, address books, competency matrices. Swap the native Repeater for ModalRepeater, leave your existing schema() array untouched, and add tableColumns() for the summary:
ModalRepeater::make('items')
->tableColumns([
Column::make('name')->label('Name'),
Column::make('price')->money('USD'),
Column::make('active')->boolean(),
])
->schema([
TextInput::make('name')->required(),
TextInput::make('price')->numeric()->required(),
Toggle::make('active'),
]);
That is the whole migration. The schema you already wrote is what renders inside the add/edit modal, so incremental cost per additional field is essentially zero — you add the field to schema() and, if it matters for scanning, one line to tableColumns().
Treat tableColumns() and schema() as two different products with two different jobs. The schema answers "what do I change?"; the columns answer "do I need to open this at all?". Pick columns that let a user decide without opening the row — status, price, name, a date. If users routinely open a row just to read a field, that field belongs in the table. If the table starts needing more than ~5 columns, you have probably outgrown a summary table.
The built-in column types cover most summary needs without custom code:
Column::make('name')->label('Product Name'),
Column::make('active')->boolean(), // checkmark / cross icon
Column::make('status')->badge('success'), // colored badge
Column::make('price')->money('USD'),
Column::make('created_at')->formatUsing(fn ($value) => $value->diffForHumans()),
Column::make('name')->width('200px'),
Relationship repeaters are a real option, not a workaround. For child records — addresses, line items, competencies — bind to Eloquent and let Filament handle load/save/delete:
ModalRepeater::make('addresses')
->relationship('addresses')
->tableColumns([
Column::make('street')->label('Street'),
Column::make('city')->label('City'),
Column::make('competency.name')->label('Competency'), // dot-notation, needs >=1.1.1
])
->schema([
TextInput::make('street')->required(),
TextInput::make('city')->required(),
Select::make('competency_id')->relationship('competency', 'name'),
]);
Since v1.1.1 the modal grid is bound to the related model, not the parent form model, so a nested Select using ->relationship() resolves correctly and pivot relationships no longer throw a LogicException. This is frequently the difference between "we need a custom child-resource CRUD screen" and "this is a modal repeater on the parent form" — a materially smaller ticket.
Ship interaction polish as later one-liners. Reorder, clone, delete, extra per-row actions, and empty-state copy all come straight from Filament's standard repeater action API. Don't spec them in v1; add them when someone asks:
ModalRepeater::make('items')
->modalColumns(3) // modal form grid columns
->modalWidth('4xl') // 'sm' | 'lg' | '4xl' | '7xl' ...
->addActionLabel('Add Item')
->editAction(fn (Action $action) => $action->label('Edit')->icon('heroicon-o-pencil'))
->extraItemActions([
Action::make('duplicate')->action(fn ($arguments) => ...),
])
->emptyLabel('No items added yet.')
->reorderable()
->cloneable()
->deletable(false);
Keep modal layout decoupled from table layout. modalColumns() and modalWidth() are the two knobs most likely to be needed on real forms — a three-column modal over a two-column table is normal and fine.
>=1.1.3 and say so in composer.json, not just in a PR description. The schema() signature break is a hard fatal, and it appears only after a Filament minor upgrade — it will not show up in your dev environment if you upgraded the package first and Filament second.formatUsing() callback receives the raw array value — a datetime field arrives as a string, so fn ($value) => $value->diffForHumans() will blow up unless the value is a castable/related model. On relationship repeaters the column resolves against the Eloquent record (v1.1.1+), which is why the README's example works there. Verify the type before writing the closure.tableColumns() keys must match schema() field names. A typo is silent — you get a blank cell, not an exception. If a column renders empty, check the key against the schema before suspecting the package.badge() takes a static color, not a mapping. For status-dependent coloring you need formatUsing() or a custom column render; there is no documented ->badge(fn ($state) => ...).reorderable() needs somewhere to store order. As with the native repeater, back it with an orderColumn()/sort field on the relationship — the modal doesn't create one for you.php artisan modal-repeater:install --help and check git status afterwards so you know whether it published config, assets, or nothing at all before committing.src/ and vendor it — but do not build core domain behaviour on top of its internals. Stick to the documented surface (ModalRepeater, Column, and inherited Filament repeater/action methods); if you find yourself subclassing or reaching into protected properties, you are taking on maintenance the package cannot promise.extraItemActions() for per-row operations, editAction()/addActionLabel() for UI copy, formatUsing() for display logic, and subclassing ModalRepeater for anything systemic. Everything else means reading the source.How can I help you explore Laravel packages today?