Filament Custom Page Example with Repeater: Pick Game Winners

Filament Custom Page Example with Repeater: Pick Game Winners
Admin
Thursday, June 15, 2023 6 mins to read
Share
Filament Custom Page Example with Repeater: Pick Game Winners

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.


The Initial Task

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:

  • Game
  • Player
  • User

This is what the DB schema looks like:

filament custom page DB schema

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.

custom page result

Important: after selecting the player in one dropdown, it should automatically be removed from all the other dropdowns.


Setup Filament Resource

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'),
];
}
}

Step 1. Empty Custom Page with a Link

So first, we need to create that custom page.

php artisan make:filament-page GameWinners

For the options, I selected these:

custom page options

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.

pick winners link

After visiting that page, we see the dummy text.

dummy pick winners page


Step 2. Winners Form with Repeater

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:

first repeater

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.

custom page result

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.