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.
Thanks alot friend. I waste two days to slove this problem.
I am making something similar and what I have is slightly different. This would not be good calculation if items are sold in 2019 for example AND invoices is loaded now with latest prizes pulled from latest prizelist. Second issue with approach is for example selling Coca-Cola cans on Pallets. Then picking number of (palets * number of cans per palet) * prize per can. Another and the biggest at least in my project is that total prize isn't re-calculatig on changing quantity or adding new item to the list. Totals refreshes only after saving and refreshing the form.
I've set afterStateUpdated on my field but then
$get('invoiceProducts')
returns null like it's reset on field value change.Hm, it can be due to a few things:
$get
and$set
are needed.If the #2 is your case, you need to debug
$get('*')
and see what paths you have. Maybe it is different and does not want to update as it is listed here. In that case, modifications are required to make the paths work again.Or if you can, add some more information - maybe I'll spot an issue from experience with this
Hi Medestas, As you have mentioned it's more likely due paths on
$get
and$set˙
. Here is short stripped down example that I have. I have thisorderingItems
collection for repeating items in repeater. There is a Placeholder just as representation with live calculation (per repeater item) that gets updated when I change quantity.It's bit harder to explain as I am getting mixed results. Here is a better explanation.
1.) If I use
collect($get('orderingItems'))
then on each page refresh$set('subtotal',..
is calculated presented properly and Ray will show $totalAmount properly. If I go ahead and change value ofpallets_quantity
, subtotal value never gets updated andray("Total: " . $totalAmount);
will show 0.2.) On other hand, if I use
collect($get('../*'))
, subtotal$set('subtotal',..
always shows 0 no matter if I change values in the fields or I reload the Form. Rayray("Total: " . $totalAmount);
will show 0 on page refresh but will show calculated values if I start changingpallets_quantity
.Looks like I've hit the limit on lenght. Here is
updateTotals
function.Okay, so a few things:
This uses
$statePath
from the livewire itself, so maybe try to play around with this calculation. And of course, you have to modify the code to pass$get
and$livewire
there :)First to thank you for looking into this and investing time to elaborate. True, I should use pastebin, appology on long posts. True I have multiple fields and different implementation. I've used Livewire to get around it but will use your example to combine and make it cleaner. Whar order actually looks like: https://pasteboard.co/wS6YEzmvcJmi.png Super nasty but working example is below. I'll work around and improove it.
I will look at your comment in depth tomorrow, but as a quick comment before sleep:
take a look at the other example that I have added here. it uses a different approach that I implemented with quantity field along others. Had similar issue and the latest example I shared - solved it!
ps. Your image did not load :(
That was quick reply :) Implementing what you've suggested now and will see how it goes. New image URL is here https://snipboard.io/nWz5Yg.jpg
I am using repeater as shown on image with Select list, just if you get a chance and point me in better direction. I need to slightly modify what's displayed on Select drop down. I've managed to add few product details in few lines and that works from searching and saving perspective. Just when form is reloaded/Edit it displays Article code in select list instead of article name. https://snipboard.io/ZtInC1.jpg I've opened discussion on Discord but still could not figure out ways around it.
Maybe some sort of Select field with HTML formatted options. Sorry for double question but related issue.
Thanks in advance!
Okay, looked at the example image that you have and... I have done something similar here: https://demo-crm.filamentexamples.com/admin/quotes/create - which also uses a repeater and a calculation based on a few fields. My latest example should work for your case, but it does indeed require quite a bit of modifications and a different approach (removing $set, $get and using state directly). At this point, I can't really give much more code, as it is a paid product (https://filamentexamples.com/project/crm-customer-management) but it should still work out as long as you change the approach :)
As for your second issue - I honestly have no idea. I did not make such a case myself, or I don't remember it :) But there's definitely some parameter that is not correct (I'd assume!)
Thank you so much for the article! Works as a charm. I have a question what if each product has an in-stock-number which I want to reduce after the order executed, how do I do that?
This can be done in multiple ways, so here's a quick overview:
In all cases, the idea is pretty simple - take the form data, do a foreach for each items and update stock value :)
Hope that helps!
Got it! Thanks alot !!
Hi Modestas, I have a question and maybe you will have suggestion on best (or better) approach. My form has repeater which is hidden until value in "Category" select field is picked. Basically, Select field and I need to select "Category" before I can fill any repeater items.
Now what I am looking for is to "lock" select field so that it can't be changed once Repeater has at least one itme added. Bigger problem is "locking" select input nad I can't just use "readOnly" or "disabled()" as it's required field. I could create another field (and hide it using css and setting scale to 0.01) which would take a value of Select field and act as a "proxy" but that would not be best or proper approach.
Would you have any suggestion maybe?
UPDATE: I have implemented it like this:
Using
Hidden
field and liwevire to check if repeater has items. I don't check if repeater's items are null, that would not matter much.Hi, this is exactly how I would have done it - just create a hidden field and fill the value!