Filament: Repeater With Live Calculations - Total Price Example

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 (see afterStateUpdated and afterStateHydrated)
  • 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 quantities
public 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...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 52 courses (943 lessons, total 46 h 42 min)
  • 75 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials