When building the Repeater field in Filament forms, you may want to perform some action or calculation whenever any of the repeater columns is changed. Let me show you how we did it in one of our Filament Examples projects of invoices.
The main logic lives in our InvoiceResource
and is mainly contained within the form()
method:
- Using a
Repeater
field to allow adding multiple items - Using the
live()
method to calculate the subtotal and total in real time - Using a custom function to trigger calculations on
items
list change, quantity change (seeafterStateUpdated
andafterStateHydrated
) - Disabling options in real-time to prevent duplicate items from being added
Here's how it looks in the code:
public static function form(Form $form): Form{ $products = Product::get(); return $form ->schema([ Section::make() ->columns(1) ->schema([ // Repeatable field for invoice items Forms\Components\Repeater::make('invoiceProducts') // Defined as a relationship to the InvoiceProduct model ->relationship() ->schema([ // Two fields in each row: product and quantity Forms\Components\Select::make('product_id') ->relationship('product', 'name') // Options are all products, but we have modified the display to show the price as well ->options( $products->mapWithKeys(function (Product $product) { return [$product->id => sprintf('%s ($%s)', $product->name, $product->price)]; }) ) // Disable options that are already selected in other rows ->disableOptionWhen(function ($value, $state, Get $get) { return collect($get('../*.product_id')) ->reject(fn($id) => $id == $state) ->filter() ->contains($value); }) ->required(), Forms\Components\TextInput::make('quantity') ->integer() ->default(1) ->required() ]) // Repeatable field is live so that it will trigger the state update on each change ->live() // After adding a new row, we need to update the totals ->afterStateUpdated(function (Get $get, Set $set) { self::updateTotals($get, $set); }) // After deleting a row, we need to update the totals ->deleteAction( fn(Action $action) => $action->after(fn(Get $get, Set $set) => self::updateTotals($get, $set)), ) // Disable reordering ->reorderable(false) ->columns(2) ]), Section::make() ->columns(1) ->maxWidth('1/2') ->schema([ Forms\Components\TextInput::make('subtotal') ->numeric() // Read-only, because it's calculated ->readOnly() ->prefix('$') // This enables us to display the subtotal on the edit page load ->afterStateHydrated(function (Get $get, Set $set) { self::updateTotals($get, $set); }), Forms\Components\TextInput::make('taxes') ->suffix('%') ->required() ->numeric() ->default(20) // Live field, as we need to re-calculate the total on each change ->live(true) // This enables us to display the subtotal on the edit page load ->afterStateUpdated(function (Get $get, Set $set) { self::updateTotals($get, $set); }), Forms\Components\TextInput::make('total') ->numeric() // Read-only, because it's calculated ->readOnly() ->prefix('$') ]) ]);} // This function updates totals based on the selected products and quantitiespublic static function updateTotals(Get $get, Set $set): void{ // Retrieve all selected products and remove empty rows $selectedProducts = collect($get('invoiceProducts'))->filter(fn($item) => !empty($item['product_id']) && !empty($item['quantity'])); // Retrieve prices for all selected products $prices = Product::find($selectedProducts->pluck('product_id'))->pluck('price', 'id'); // Calculate subtotal based on the selected products and quantities $subtotal = $selectedProducts->reduce(function ($subtotal, $product) use ($prices) { return $subtotal + ($prices[$product['product_id']] * $product['quantity']); }, 0); // Update the state with the new values $set('subtotal', number_format($subtotal, 2, '.', '')); $set('total', number_format($subtotal + ($subtotal * ($get('taxes') / 100)), 2, '.', ''));}
If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.
No comments or questions yet...