Filament Appointment Booking: Re-Use Admin Panel Form on Public Page

Filament Appointment Booking: Re-Use Admin Panel Form on Public Page
Admin
Thursday, August 17, 2023 7 mins to read
Share
Filament Appointment Booking: Re-Use Admin Panel Form on Public Page

Imagine you need a system for booking appointments: doctor, hair salon, or cart racing track. In this tutorial, we will create exactly that, with a 2-in-1 demo: how to build a dynamic form in Filament and how to reuse it outside the adminpanel for non-logged-in users to be able to book an appointment.

Specifically, we will make bookings for cart racing tracks. But the same logic could be applied to a different company. Just use room numbers or doctor names instead of track names.

As usual, the link to the complete repository will be at the end of the tutorial.

Here's the Table of Contents for this detailed step-by-step tutorial.

Section 1. Filament Panel: Reservation Table/Form

  1. Install Filament
  2. Set Up User to Access Admin Panel
  3. Filament CRUD Resource for Tracks
  4. Filament: Dynamic Reservation Form
  5. Filament: Table of Reservations
  6. Filament: Disable Reservation Edit

Section 2. Public Form for Reservation

  1. Install And Configure TailwindCSS
  2. Set Up Main App Layout
  3. Use Service In Filament ReservationResource
  4. Create Livewire ReservationForm Component

Setup: DB Models and Migrations

Before getting to Filament, we need to create the DB structure in Laravel.

  • Tracks will have many Reservations
  • Reservation belongs to a Track and also belongs to a User

Here are our migrations and Models:

Tracks table:

Schema::create('tracks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});

app/Models/Track.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
// ...
 
protected $fillable = ['title'];
 
public function reservations(): HasMany
{
return $this->hasMany(Reservation::class);
}

Reservations table:

use App\Models\Track;
use App\Models\User;
 
// ...
 
Schema::create('reservations', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(Track::class)->constrained()->cascadeOnDelete();
$table->datetime('start_time');
$table->datetime('end_time');
$table->timestamps();
});

app/Models/Reservation.php

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
// ...
 
protected $fillable = [
'user_id',
'track_id',
'start_time',
'end_time'
];
 
public function track(): BelongsTo
{
return $this->belongsTo(Track::class);
}
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

app/Models/User.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
public function reservations(): HasMany
{
return $this->hasMany(Reservation::class);
}

We also seed the admin user and five racing tracks.

database/seeders/DatabaseSeeder.php

use App\Models\Track;
use App\Models\User;
 
public function run(): void
{
User::create([
'name' => 'Admin',
'email' => 'admin@admin.com',
'password' => bcrypt('password'),
]);
 
$tracks = [
'Track 1',
'Track 2',
'Track 3',
'Track 4',
'Track 5',
];
foreach ($tracks as $track) {
Track::create(['title' => $track]);
}
}

And finally, migrate and seed the database.

php artisan migrate:fresh --seed

Ok, we have the database ready. Now it's time to build the admin panel.


1. Filament Panel: Reservation Table/Form

First, we will build a Filament project so that the administrator can place a booking. In the following Section 2, we will re-use that form to accept reservations from the outside.

Our form will be dynamic: the admin chooses the date, sees the available timeslots, and then picks the radio button selected.

Reservations


1.1. Install Filament

Since Livewire v3 is still in beta, set the minimum-stability in your composer.json to dev.

composer.json

"minimum-stability": "dev"

Install the Filament Panel Builder by running the following commands in your Laravel project directory.

composer require filament/filament:"^3.0-stable" -W
 
php artisan filament:install --panels

--with-all-dependencies (-W): Update also dependencies of packages in the argument list, including those which are root requirements.


1.2. Set Up User to Access Admin Panel

To set up your User Model to access Filament in non-local environments, you must implement the FilamentUser contract and define the canAccessPanel() method.

use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
 
class User extends Authenticatable implements FilamentUser
{
// ...
 
public function canAccessPanel(Panel $panel): bool
{
return true;
}
}

The canAccessPanel() method returns true or false depending on whether the user can access the $panel. In this example, we let all users access the panel.

When using Filament, you do not need to define any routes manually. Now you can log in by visiting the /admin path.

Filament Login

After logging in, you should see the following dashboard.

Filament Dashboard

Our dashboard is empty, so let's make a section for managing tracks.


1.3. Filament CRUD Resource for Tracks

Let's allow admins to manage Tracks.

In Filament, resources are static classes used to build CRUD interfaces for your Eloquent models. They describe how administrators can interact with data from your panel using tables and forms.

Use the following artisan command to create a new Filament resource for the Track Model.

php artisan make:filament-resource Track

Then in the table() method columns() call, define TextColumn to display title.

app/Filament/Resources/TrackResource.php

use Filament\Tables\Columns\TextColumn;
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title'),
])
// ...
}

That's it, and when you refresh the dashboard, you should see a new Tracks entry in the menu.

List Tracks

You can explore different column types on Filament Table Columns Documentation.

Now let's define input fields for creating and editing Track in the form() method.

app/Filament/Resources/TrackResource.php

use Filament\Forms\Components\TextInput;
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')->required()->maxLength(255),
]);
}

Validation rules can be applied to the TextInput by calling required() and maxLength() methods.

Now we can edit and create new tracks in just a minute.

Edit Track

Filament will try to save model attributes under the same name you defined by default.

Possible form components and their options are on Filament Fields Documentation.


1.4. Filament: Dynamic Reservation Form

Let's create a Reservation resource.

php artisan make:filament-resource Reservation

And define the create form's date and track fields. We use the getAvailableReservations() static method to get available reservations. Note that in the resource, all methods are static.

app/Filament/Resources/ReservationResource.php

use App\Models\Track;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Radio;
use Filament\Forms\Get;
 
// ...
 
public static function form(Form $form): Form
{
$dateFormat = 'Y-m-d';
 
return $form
->schema([
DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),
Radio::make('track')
->options(fn (Get $get) => self::getAvailableReservations($get))
->hidden(fn (Get $get) => ! $get('date'))
->required()
->columnSpan(2),
]);
}
 
public static function getAvailableReservations(Get $get): array
{
$date = Carbon::parse($get('date'));
$startPeriod = $date->copy()->hour(14);
$endPeriod = $date->copy()->hour(16);
$times = CarbonPeriod::create($startPeriod, '1 hour', $endPeriod);
$availableReservations = [];
 
$tracks = Track::with([
'reservations' => function ($q) use ($startPeriod, $endPeriod) {
$q->whereBetween('start_time', [$startPeriod, $endPeriod]);
},
])
->get();
 
foreach ($tracks as $track) {
$reservations = $track->reservations->pluck('start_time')->toArray();
 
$availableTimes = $times->copy()->filter(function ($time) use ($reservations) {
return ! in_array($time, $reservations) && ! $time->isPast();
})->toArray();
 
foreach ($availableTimes as $time) {
$key = $track->id . '-' . $time->format('H');
$availableReservations[$key] = $track->title . ' ' . $time->format('H:i');
}
}
 
return $availableReservations;
}

Now let's get through all the logic.

DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),

By default DatePicker component uses the browser's default date picker. We want the date picker consistent, so we use the native(false) method.

Reservations can be made from the current date to two weeks upfront. We can limit that using minDate() and maxDate() methods. Then we define the date format using the format() method.

Initially, we want to display only the date field and will show available reservations only when the date field value is present. First, we need to ensure that the state is sent to the server immediately when the user interacts with the field by using the live() method.

We use the hidden() method on a radio field, which accepts a boolean or closure to show or hide this field.

use Filament\Forms\Get;
 
// ...
 
Radio::make('track')
->options(fn (Get $get) => self::getAvailableReservations($get))
->hidden(fn (Get $get) => ! $get('date'))
->required()
->columnSpan(2),

We can retrieve the value of another field from within a callback using a $get parameter by injecting the Get class.

In the same way, we define closure for options() to get available reservations for a selected date.

Now let's look into the getAvailableReservations() method. First, we make a Carbon instance of the date field value.

$date = Carbon::parse($get('date'));

Then specify the starting and ending hours we want to reserve by making a copy of the $date.

$startPeriod = $date->copy()->hour(14);
$endPeriod = $date->copy()->hour(16);

Now we need to make a list between those hours with 1-hour intervals.

$times = CarbonPeriod::create($startPeriod, '1 hour', $endPeriod);

To simplify, this will result in Carbon objects with dates like this:

[
'2023-08-11 14:00:00',
'2023-08-11 15:00:00',
'2023-08-11 16:00:00',
]

The next objective is to filter those times for a selected date already reserved. Let's get all the Tracks with reservations for the selected date. We can use the whereBetween() Eloquent method.

$tracks = Track::with([
'reservations' => function ($q) use ($startPeriod, $endPeriod) {
$q->whereBetween('start_time', [$startPeriod, $endPeriod]);
},
])->get();

Then we iterate through each Track and get all the Reservation starting times.

$reservations = $track->reservations->pluck('start_time')->toArray();

The next step is to exclude all the $reservations values from all possible dates from the $times variable, and also, we don't include period values if the date is in the past. If now is 14:05, you shouldn't be able to book a track for 14:00.

$availableTimes = $times->copy()
->filter(function ($time) use ($reservations) {
return ! in_array($time, $reservations) && ! $time->isPast();
})->toArray();

Now we have $track and $availableTimes for Reservations. The problem is that we want to store both values in a single radio option. We can combine track id and hour value in an array key.

[
'<track_id>-<hour>' => 'Radio button text...'
// ...
]

For that, we can do another loop.

foreach ($availableTimes as $time) {
$key = $track->id . '-' . $time->format('H');
 
$availableReservations[$key] = $track->title . ' ' . $time->format('H:i');
}

And finally, return $availableReservations. It may look similar to this:

[
'1-15' => 'Track 1 15:00'
'1-16' => 'Track 1 16:00'
'2-14' => 'Track 2 14:00'
'2-15' => 'Track 2 15:00'
'3-14' => 'Track 3 14:00'
'3-15' => 'Track 3 15:00'
'4-15' => 'Track 4 15:00'
'4-16' => 'Track 4 16:00'
'5-14' => 'Track 5 14:00'
'5-15' => 'Track 5 15:00'
'5-16' => 'Track 5 16:00'
]

If you try to save this form, it won't work. The Reservation Model has the following fields.

['user_id', 'track_id', 'start_time', 'end_time']

The problem is that Filament tries to map fields to attributes with the same name by default, but we only have date and track (track id with hour) fields. We need to mutate submitted data before saving it.

Add the mutateFormDataBeforeCreate() method CreateReservation class.

app/Filament/Resources/ReservationResource/Pages/CreateReservation.php

use Carbon\Carbon;
 
// ...
 
protected function mutateFormDataBeforeCreate(array $data): array
{
$date = Carbon::parse($data['date']);
[$trackId, $hour] = explode('-', $data['track']);
$startTime = $date->copy()->hour($hour);
$endTime = $startTime->copy()->addHour();
 
$dateTimeFormat = 'Y-m-d H:i:s';
 
return [
'user_id' => auth()->id(),
'track_id' => $trackId,
'start_time' => $startTime->format($dateTimeFormat),
'end_time' => $endTime->format($dateTimeFormat),
];
}

We use array destructuring on the track field to separate track id and hour into variables.

[$trackId, $hour] = explode('-', $data['track']);

We can make the Carbon instance again to set $startTime easily from the submitted data. The $endTime is set automatically by adding one hour to $startTime.

$date = Carbon::parse($data['date']);
$startTime = $date->copy()->hour($hour);
$endTime = $startTime->copy()->addHour();

The user_id is also set automatically by the currently logged-in user.

Finally, we return an array of attributes we want to save on a Model.

return [
'user_id' => auth()->id(),
'track_id' => $trackId,
'start_time' => $startTime->format($dateTimeFormat),
'end_time' => $endTime->format($dateTimeFormat),
];

1.5. Filament: Table of Reservations

Let's display the reservations table in the same ReservationResource.

List Reservations

Update the table() method in ReservationResource.

app/Filament/Resources/ReservationResource.php

use Filament\Tables\Columns\TextColumn;
 
// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name'),
TextColumn::make('track.title'),
TextColumn::make('start_time')->dateTime('Y-m-d H:i'),
TextColumn::make('end_time')->dateTime('Y-m-d H:i'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
])
->defaultSort('start_time', 'desc');
}

We can display which user made the Reservation using the dot notation relationshipName.columnName.

TextColumn::make('user.name')

We can format dates using the dateTime method that accepts format as an argument.

TextColumn::make('start_time')->dateTime('Y-m-d H:i')

We can also alter the default sort using the defaultSort() method.

->defaultSort('start_time', 'desc')

1.6. Filament: Disable Reservation Edit

This is the final thing in our admin panel.

After creating a new reservation user gets redirected to the edit page. According to our requirements, the user should not be able to edit Reservations. Let's disable the edit page.

Update the table() and getPages() methods in ReservationResource.

app/Filament/Resources/ReservationResource.php

public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name'),
TextColumn::make('track.title'),
TextColumn::make('start_time')->dateTime('Y-m-d H:i'),
TextColumn::make('end_time')->dateTime('Y-m-d H:i'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->actions([])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
])
->defaultSort('start_time', 'desc');
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListReservations::route('/'),
'create' => Pages\CreateReservation::route('/create'),
'edit' => Pages\EditReservation::route('/{record}/edit'),
];
}

Ok great, so we have a fully working admin panel. Now, let's try to re-use the same form outside of it, as a public reservation page.


2. Public Form for Reservation

What if we want to make a reservation form not as a part of the Filament admin panel but as a public page? We will learn how to create Livewire Component to use Filament Forms.

Public Reservations

Requirements

  • Form must have additional name and email fields.
  • Create a new user before making a reservation.
  • New reservations will be attached to the created user.
  • Clear the form after submission.
  • Show notification after successful reservation.

2.1. Install And Configure TailwindCSS

Before we go any further, let's first install TailwindCSS.

npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss autoprefixer --save-dev

Create a new TailwindCSS config.

tailwind.config.js

import preset from './vendor/filament/support/tailwind.config.preset'
 
export default {
presets: [preset],
content: [
'./app/Filament/**/*.php',
'./resources/views/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
}

Create a new CSS file. The x-cloak attribute should hide DOM elements. Filament uses this Alpine feature to show/hide form elements.

resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer components {
[x-cloak] {
display: none !important;
}
}

Create a new PostCSS config.

postcss.config.js

export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

And update the Vite config to enable auto-refresh when we update Livewire components.

vite.config.js

import { defineConfig } from 'vite';
import laravel, { refreshPaths } from 'laravel-vite-plugin';
 
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: [
...refreshPaths,
'app/Livewire/**',
],
}),
],
});

2.2. Set Up Main App Layout

We have no application layout since we didn't install any starter kits. Create the AppLayout Blade component.

app/View/Components/AppLayout.php

namespace App\View\Components;
 
use Illuminate\View\Component;
use Illuminate\View\View;
 
class AppLayout extends Component
{
public function render(): View
{
return view('layouts.app');
}
}

Create Blade file for the AppLayout component.

resources/views/layouts/app.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
 
<meta name="application-name" content="{{ config('app.name') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
 
<title>{{ config('app.name') }}</title>
 
@filamentStyles
@vite('resources/css/app.css')
</head>
 
<body class="antialiased bg-gray-100">
<div class="max-w-7xl mx-auto">
{{ $slot }}
</div>
 
@filamentScripts
@vite('resources/js/app.js')
@livewire('notifications')
</body>
</html>

The {{ $slot }} part is where your content will appear when using this layout.

If we want to use Filament Notification pop-ups, it is essential to add @livewire('notifications') component.

Then create a homepage.blade.php file with dummy content.

resources/views/homepage.blade.php

<x-app-layout>
App Layout Works!
</x-app-layout>

And update the routes file to display this page when we navigate to the site's root / path.

routes/web.php

Route::get('/', function () {
return view('welcome');
return view('homepage');
});

Finally, let's compile CSS using npm.

npm run dev

You should see the "App Layout Works!" title.


2.3. Use Service In Filament ReservationResource

Moving the getAvailableReservations() method from ReservationResource would be better because we want to also use that logic in the custom Livewire component we are about to create.

Create the ReservationService class.

app/Services/ReservationService.php

namespace App\Services;
 
use App\Models\Track;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
 
class ReservationService
{
public function getAvailableReservations(string $date): array
{
$date = Carbon::parse($date);
$startPeriod = $date->copy()->hour(14);
$endPeriod = $date->copy()->hour(16);
$times = CarbonPeriod::create($startPeriod, '1 hour', $endPeriod);
$availableReservations = [];
 
$tracks = Track::with([
'reservations' => function ($q) use ($startPeriod, $endPeriod) {
$q->whereBetween('start_time', [$startPeriod, $endPeriod]);
},
])
->get();
 
foreach ($tracks as $track) {
$reservations = $track->reservations->pluck('start_time')->toArray();
 
$availableTimes = $times->copy()->filter(function ($time) use ($reservations) {
return ! in_array($time, $reservations);
})->toArray();
 
foreach ($availableTimes as $time) {
$key = $track->id . '-' . $time->format('H');
$availableReservations[$key] = $track->title . ' ' . $time->format('H:i');
}
}
 
return $availableReservations;
}
}

Update the form() method and delete the getAvailableReservations() method in ReservationResource.

app/Filament/Resources/ReservationResource.php

use App\Models\Track;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use App\Services\ReservationService;
 
// ...
 
public static function form(Form $form): Form
{
// ...
Radio::make('track')
->options(fn (Get $get) => self::getAvailableReservations($get))
->options(fn (Get $get) => (new ReservationService())->getAvailableReservations($get('date')))
->hidden(fn (Get $get) => ! $get('date'))
->required()
->columnSpan(2),
// ...
 
public static function getAvailableReservations(Get $get): array
{
$date = Carbon::parse($get('date'));
$startPeriod = $date->copy()->hour(14);
$endPeriod = $date->copy()->hour(16);
$times = CarbonPeriod::create($startPeriod, '1 hour', $endPeriod);
$availableReservations = [];
 
$tracks = Track::with([
'reservations' => function ($q) use ($startPeriod, $endPeriod) {
$q->whereBetween('start_time', [$startPeriod, $endPeriod]);
},
])
->get();
 
foreach ($tracks as $track) {
$reservations = $track->reservations->pluck('start_time')->toArray();
 
$availableTimes = $times->copy()->filter(function ($time) use ($reservations) {
return ! in_array($time, $reservations) && ! $time->isPast();
})->toArray();
 
foreach ($availableTimes as $time) {
$key = $track->id . '-' . $time->format('H');
$availableReservations[$key] = $track->title . ' ' . $time->format('H:i');
}
}
 
return $availableReservations;
}

2.4. Create Livewire ReservationForm Component

It is time to create a reservation form for the homepage. Run the make:livewire-form Artisan command to create a new Livewire component.

php artisan make:livewire-form ReservationForm
 
┌ What is the model name? ─────────────────────────────────────┐
│ Reservation │
└──────────────────────────────────────────────────────────────┘
 
┌ Which namespace would you like to create this in? ───────────┐
│ Create │
└──────────────────────────────────────────────────────────────┘
 
INFO Successfully created ReservationForm!

Update the ReservationForm.

app/Livewire/ReservationForm.php

namespace App\Livewire;
 
use App\Models\Reservation;
use App\Models\User;
use App\Services\ReservationService;
use Carbon\Carbon;
use Filament\Forms;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Illuminate\Contracts\View\View;
use Livewire\Component;
 
class ReservationForm extends Component implements HasForms
{
use InteractsWithForms;
 
public ?array $data = [];
 
protected ReservationService $service;
 
public function __construct()
{
$this->service = new ReservationService();
}
 
public function mount(): void
{
$this->form->fill();
}
 
public function form(Form $form): Form
{
$dateFormat = 'Y-m-d';
 
return $form
->schema([
TextInput::make('name')
->required(),
TextInput::make('email')
->email()
->unique('users', 'email')
->required(),
DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),
Radio::make('track')
->options(fn (Get $get) => $this->service->getAvailableReservations($get('date')))
->hidden(fn (Get $get) => ! $get('date'))
->required(),
])
->statePath('data')
->model(Reservation::class);
}
 
public function create(): void
{
$data = $this->form->getState();
 
$date = Carbon::parse($data['date']);
[$trackId, $hour] = explode('-', $data['track']);
$startTime = $date->copy()->hour($hour);
$endTime = $startTime->copy()->addHour();
$dateTimeFormat = 'Y-m-d H:i:s';
 
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => '',
])->reservations()->create([
'track_id' => $trackId,
'start_time' => $startTime->format($dateTimeFormat),
'end_time' => $endTime->format($dateTimeFormat),
]);
 
Notification::make()
->success()
->title('Reservation has been created')
->seconds(5)
->send();
 
$this->form->fill();
}
 
public function render(): View
{
return view('livewire.reservation-form');
}
}

Now let's go through the code. To use Filament Forms in a Livewire Component, it must implement the HasForms interface and use the InteractsWithForms trait.

use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Concerns\InteractsWithForms;
 
// ...
 
class ReservationForm extends Component implements HasForms
{
use InteractsWithForms;
 
// ...

We define a public Livewire property to store the form's data. In our example, we'll call this $data.

public ?array $data = [];

In the form() method, we add form schema as in ReservationResource and tell Filament to store the form data in the $data property using the statePath() method.

public function form(Form $form): Form
{
$dateFormat = 'Y-m-d';
 
return $form
->schema([
TextInput::make('name')
->required(),
TextInput::make('email')
->email()
->unique('users', 'email')
->required(),
DatePicker::make('date')
->native(false)
->minDate(now()->format($dateFormat))
->maxDate(now()->addWeeks(2)->format($dateFormat))
->format($dateFormat)
->required()
->live(),
Radio::make('track')
->options(fn (Get $get) => $this->service->getAvailableReservations($get('date')))
->hidden(fn (Get $get) => ! $get('date'))
->required(),
])
->statePath('data')
->model(Reservation::class);
}

The form is initialized with $this->form->fill() in the mount() method. This is imperative for every form that you build, even if it doesn't have any initial data.

public function mount(): void
{
$this->form->fill();
}

The create() method will handle form processing. The $this->form->getState() statement retrieves the form data.

$data = $this->form->getState();

Notifications can also be handled from the back end using the Filament\Notifications\Notification class. In this example, we display a success message for five seconds. It is essential to call send() at the end; otherwise, it won't appear.

use Filament\Notifications\Notification;
 
// ...
 
Notification::make()
->success()
->title('Reservation has been created')
->seconds(5)
->send();

After processing, we call $this->form->fill() again to re-initialize the form with empty values to clear the form.

$this->form->fill();

Now update the Livewire component's Blade file.

resources/views/livewire/reservation-form.blade.php

<div class="mt-16">
<h2 class="text-2xl font-bold tracking-tight mb-4">Create Reservation</h2>
 
<div class="bg-white p-6 rounded-md shadow-sm">
<form wire:submit="create">
{{ $this->form }}
 
<button type="submit" class="bg-primary-600 text-white font-bold rounded-md px-3 py-2 tracking-tight mt-8">
Create
</button>
</form>
</div>
 
<x-filament-actions::modals />
</div>

And include it on the homepage.

resources/views/homepage.blade.php

<x-app-layout>
App Layout Works!
@livewire('reservation-form')
</x-app-layout>

The following form should appear when you visit the homepage.

Public Reservations

When you submit the form, a notification will be shown.

Reservation Created Notification

In the list, we should see new reservations made by other users. In this example, John Doe.

John Reservation


The full source code is available in this GitHub repository.


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.