Filament is a great admin panel system, but it often confuses users how to create a custom non-CRUD page in Filament that wouldn't be a typical Resource? This tutorial will provide an example.
In this tutorial, we will create a custom Filament page with just one form. We will use a Repeater field for selecting the winners for the game.
This tutorial has three Models:
This is what the DB schema looks like:
Our task is to build a custom page to pick the winner players for a particular game. So it's not a typical "Game Edit" page but something different.
It will be a Repeater with 10 dropdowns.
Important: after selecting the player in one dropdown, it should automatically be removed from all the other dropdowns.
We will have only one Filament Resource for Games.
app/Filament/Resources/GameResource.php:
use App\Models\Game;use Filament\Resources\Form;use Filament\Resources\Table;use Filament\Resources\Resource;use Filament\Tables\Columns\TextColumn;use Filament\Forms\Components\TextInput; class GameResource extends Resource{ protected static ?string $model = Game::class; public static function form(Form $form): Form { return $form ->schema([ TextInput::make('name') ->required(), ]); } public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name'), ]); } public static function getPages(): array { return [ 'index' => Pages\ListGames::route('/'), 'create' => Pages\CreateGame::route('/create'), 'edit' => Pages\EditGame::route('/{record}/edit'), ]; }}
So first, we need to create that custom page.
php artisan make:filament-page GameWinners
For the options, I selected these:
This command created two files:
App/Filament/Resources/GameResource/Pages/GameWinners.php
where all the logic goes.resources/views/filament/resources/game-resources/pages/game-winners.blade.php
where the front-end part goes.For now, let's add a dummy text to the Blade file.
resources/views/filament/resources/game-resources/pages/game-winners.blade.php:
<x-filament::page> Winners form will be here</x-filament::page>
Next, we need to add a Route to the GameResource
and prepend action to the table so that link would be shown.
app/Filament/Resources/GameResource/Pages/GameWinners.php:
use Filament\Tables;use App\Filament\Resources\GameResource\Pages; class GameResource extends Resource{ // ... public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name'), ]) ->prependActions([ Tables\Actions\Action::make('Pick winners') ->color('success') ->url(fn(Game $record): string => self::getUrl('winners', ['record' => $record])) ]); } public static function getPages(): array { return [ 'index' => Pages\ListGames::route('/'), 'create' => Pages\CreateGame::route('/create'), 'edit' => Pages\EditGame::route('/{record}/edit'), 'winners' => Pages\GameWinners::route('/{record}/winners'), ]; }}
So now, on the games page, we see an action Pick winners
.
After visiting that page, we see the dummy text.
Now that we have a custom page.
What you need to know about Filament is that every custom page is just a Livewire component. This means you can do everything the same way you would without Filament.
So, we need to prepare this Livewire component to use Filament Forms. According to the docs, it's straightforward: we need to implement the HasForms
interface and use the InteractsWithForms
trait.
app/Filament/Resources/GameResource/Pages/GameWinners.php:
class GameWinners extends Page implements Forms\Contracts\HasForms { use Forms\Concerns\InteractsWithForms; // ...}
And we need to render the form in the Blade file.
resources/views/filament/resources/game-resources/pages/game-winners.blade.php:
<x-filament::page> <form wire:submit.prevent="submit"> {{ $this->form }} <x-filament-support::button type="submit" class="mt-4"> Save </x-filament-support::button> </form> Winners form will be here </x-filament::page>
Notice: I'm using the button from the Filament itself. This way, the styling is the same. Also, for example, if you have a file upload field while the file is uploading, the button will be disabled.
Next, let's add a Repeater to the form. For every game, we will allow to select ten players.
app/Filament/Resources/GameResource/Pages/GameWinners.php:
use App\Models\Player; class GameWinners extends Page implements Forms\Contracts\HasForms{ // ... public function mount(): void { $this->form->fill(); } protected function getFormSchema(): array { return [ Forms\Components\Repeater::make('players') ->schema([ Forms\Components\Select::make('name') ->options(Player::pluck('name', 'id')->toArray()) ->reactive() ->required() ]) ->disableLabel() ->defaultItems(10) ->disableItemCreation() ->disableItemDeletion() ->disableItemMovement() ]; }}
The essential part here is in the mount()
method. If we don't initialize the form on the first load, we won't see the default ten repeaters.
This is the result now:
Now, let's take care of the reloading effect: after selecting the player, that player wouldn't be available in other inputs.
app/Filament/Resources/GameResource/Pages/GameWinners.php:
class GameWinners extends Page implements Forms\Contracts\HasForms{ // ... protected function getFormSchema(): array { return [ Forms\Components\Repeater::make('players') ->schema([ Forms\Components\Select::make('name') ->options(Player::pluck('name', 'id')->toArray()) ->options(function (callable $get) { $players = $get('../../players'); $idsAlreadyUsed = []; foreach($players as $repeater) { if(! in_array($repeater['name'], $idsAlreadyUsed) && $repeater['name'] !== null && $repeater['name'] != $get('name')) { $idsAlreadyUsed[] = $repeater['name']; } } return Player::whereNotIn('id', $idsAlreadyUsed)->pluck('name', 'id')->toArray(); }) ->reactive() ->required() ]) ->disableLabel() ->defaultItems(10) ->disableItemCreation() ->disableItemDeletion() ->disableItemMovement() ]; }}
So what do we do here? First, we get all the players. And because this is a repeater, the values aren't in the root of the array. You can read more about this in the docs.
Then we go through each player and check if it isn't already selected, if the value isn't null, and if the value chosen isn't the same as just select. If those checks are correct, we add the selected value's ID to the idsAlreadyUsed
array.
And lastly, return the players list where the ID isn't in the idsAlreadyUsed
list.
All that is left is to save results into the DB. In the form, we have set after submit to call the submit()
method.
The game ID we can get from the request. Next, we go through each player and make an array to insert records into the DB. The insert
instead of a create
here is used so we wouldn't make ten SQL inserts. And then just sending the notification and redirecting to the GameResource
index page.
app/Filament/Resources/GameResource/Pages/GameWinners.php:
use App\Models\Result;use Filament\Notifications\Notification; class GameWinners extends Page implements Forms\Contracts\HasForms{ // ... public function submit() { $gameId = explode('/', request()->fingerprint['path'])[2]; $results = []; $players = $this->form->getState()['players']; foreach ($players as $key => $item) { $results[] = [ 'user_id' => auth()->id(), 'game_id' => $gameId, 'position' => $key + 1, 'player_id' => $item['name'], 'created_at' => now(), 'updated_at' => now(), ]; } Result::insert($results); Notification::make() ->title('Winners has been set') ->success() ->send(); $this->redirect(GameResource::getUrl()); } // ...}
The GitHub repository for this tutorial can be found here.
If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.