In the process of while we're building simple tables, you got already familiar with three types of actions:
- Edit/Delete buttons in a column
- Create a button on top
- Multi-checkbox Bulk Actions on top-left
In this lesson, we will look at how to create custom actions for all those cases and more.
Edit/Delete + Extra Buttons
Let's try to add another button to the Orders table, like "Mark as Complete," which would require confirmation and would change the status of the order.
Migration:
Schema::table('orders', function (Blueprint $table) { $table->boolean('is_completed')->default(false);});
Also, we make it fillable:
app/Models/Order.php:
class Order extends Model{ use HasFactory; protected $fillable = [ 'user_id', 'product_id', 'price', 'is_completed', ];
Now, here's what action we can add to the table:
app/Filament/Resources/OrderResource.php:
return $table ->columns([ // ... ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\Action::make('Mark Completed') ->requiresConfirmation() ->icon('heroicon-o-check-badge') ->hidden(fn (Order $record) => $record->is_completed) ->action(fn (Order $record) => $record->update(['is_completed' => true])), ])
Here's the visual result:
Pretty sure that the code above is self-explanatory, but here are a few things to notice:
-
make('Mark Completed')
parameter means the label of the link -
requiresConfirmation()
will show the confirmation modal, like in the Delete action -
icon
can be chosen from the Blade Icons package - methods
hidden()
andaction()
both accept callback functions -
hidden()
should return true/false with the condition when the link should be hidden. In the screenshot above, you see one record as already completed
If you want to save some space in a table row, you may hide the actions under a vertical dots icon with ActionGroup()
:
return $table ->columns([ // ... ]) ->actions([ Tables\Actions\ActionGroup::make([ Tables\Actions\EditAction::make(), Tables\Actions\Action::make('Mark Completed') ->requiresConfirmation() ->hidden(fn (Order $record) => $record->is_completed) ->icon('heroicon-o-check-badge') ->action(fn (Order $record) => $record->update(['is_completed' => true])), ]) ])
This is how it looks now.
So this is how you add any custom action to your table "Actions column".
Table Action with Modal Edit Form
What if you want to have some custom mini-edit form: you click the link in the table, there's a modal form with a few fields (but not full edit), and you submit?
Let's implement it with "Mark as completed" but as a modal form with a checkbox. Not a very practical example, I know, but the goal is for you to understand the idea of how it works.
->actions([ Tables\Actions\EditAction::make(), Tables\Actions\Action::make('Change is completed') ->icon('heroicon-o-check-badge') ->fillForm(function (Order $order) { return ['is_completed' => $order->is_completed]; }) ->form([ Forms\Components\Checkbox::make('is_completed'), ]) ->action(function (Order $order, array $data): void { $order->update(['is_completed' => $data['is_completed']]); }),
Here's the visual result:
As you can see, you need to fill in three things:
- form fields
- fill that form (it's not filled in automatically)
- action to update the record after submit
Bulk Action: Add More to "Delete Selected"
By default, Filament shows a checkbox for each table record and allows to bulk-process them somehow. The only default function is "Bulk Delete", but you can add your own.
In the same table()
method, there's a method bulkActions()
where you pass the array of the Actions you want.
Let's try to implement "Bulk Mark as Completed".
Here's the code:
use Illuminate\Database\Eloquent\Collection; // ... return $table ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), Tables\Actions\BulkAction::make('Mark as Completed') ->icon('heroicon-o-check-badge') ->requiresConfirmation() ->action(fn (Collection $records) => $records->each->update(['is_completed' => true])) ->deselectRecordsAfterCompletion() ]), ])
Here's how it looks:
Instead of Action::make()
, we have a BulkAction::make()
, but other options are almost identical.
The difference in the action()
method is that we work with Eloquent Collection and not one specific record.
Also, the method deselectRecordsAfterCompletion()
makes sure that the selected checkboxes are de-selected after the action is processed.
Header Actions for All Table
There's also one more place where you can place Actions: on top of the table. It's called "Header Actions".
A pretty typical example would be to exporting records to Excel/CSV or if you just want to make other bulk actions more visible instead of hiding them under Bulk Actions.
Also, you can put a "Create new record" button there to make it more visible than the default top-right corner.
->headerActions([ Tables\Actions\Action::make('New Order') ->url(fn (): string => OrderResource::getUrl('create')),])
Here's the button on top now:
So, How to HIDE the default "New order" button on the top-right corner?
I get it!Comment out the line of method "getHeaderActions" in file ListOrders.php.
Yes, I cover that in the later lesson :)
Hi Povilas,
I get the following error when I perform the bulk action: App\Filament\Resources\OrderResource::App\Filament\Resources{closure}(): Argument #1 ($records) must be of type App\Filament\Resources\Collection, Illuminate\Database\Eloquent\Collection given, called in /Users/tobias/Sites/filament-
even when I manually reference: App\Filament\Resources\Collection,
what could it cause ?
Without code can't help
Actually you can help without seeing the code, because the clues are right there in the exception message ;)
Look at what it says: a
Collection
of typeIlluminate\Database\Eloquent\Collection
has been passed to the closure, but the closure's first argument is required to be a Collection of typeApp\Filament\Resources\Collection
.So the most likely explanation is that you've defined the closure parameter as
Collection $record
and in your uses clause at the top of your resource,Collection
is defined asuse App\Filament\Resources\Collection;
.Therefore, either change this to
use Illuminate\Database\Eloquent\Collection
or specify the fully-qualified name in the closure parameter definition:Illuminate\Database\Eloquent\Collection $record
.If you choose to change the
use
clause, you may find that the otherCollection
type is used elsewhere in your resource. In that case, you can alias the Eloquent collection by:use App\Filament\Resources\Collection as EloquentCollection;
then useEloquentCollection $record
as the closure parameter.I'm sure there's a perfectly good explanation for this, but if the error just said (paraphrasing) that it had been given
App\Filament\Resources\Collection
but that it really wantedIlluminate\Database\Eloquent\Collection
then it'd be a lot easier to debug.Perhaps it's just me, but the way that error reads, it seems like it's received
Illuminate\Database\Eloquent\Collection
but wantsApp\Filament\Resources\Collection
Putting aside for the moment that there's actually no such thing as
App\Filament\Resources\Collection
it is still quite confusing.But as I said above, I suspect there's a very sensible explanation as to why it has to be like this. So any additional pointers you can provide about how to properly read and understand this type of error message in the future would be really useful. Perhaps there's a key point or concept that one needs to grasp first to make it obvious?
Can I make a model action load PayPal or Braintree’s (that’s all that is supported where I live) JS API to take a payment.
Or to simplify the request - can the modal load some JS which returns a response that performs the action and closes the modal?
I suppose this would be as good a place as any to ask this question: When following these instructions and making the "Mark Completed" button, why is there such a long delay between pressing the button and the confirmation dialogue appearing?
From pressing the button to seeing the confirmation dialog, I'm waiting about 1-2 seconds. Yet when the confirmation dialogue finally appears, debugbar shows just 120ms.
Debugbar actually IS the reason of slowness.
Watch this video for more details.
Ahhh.... yes, that's funny. And, also, makes perfect sense :-)