Filament 3 has authentication features that you can easily enable, like the Profile page. But its default design is the same as the login/register page. What if we want to use the full-page design, with menus, for the Edit Profile page, too? Let's see how to do it.
This is how it looks by default:
What if we want to use the "full" Filament design with menus on the left? Also, we can go one step further and have form layouts similar to Laravel Breeze:
So, we have two goals for this tutorial:
- Use the full-page Filament design with custom pages
- Create multiple forms on one Filament page
Let's go!
Custom Page for Edit Profile Page
So first, let's create a new custom page.
php artisan make:filament-page EditProfile --type=custom
Don't choose any Resource.
This command created two files:
-
App\Filament\Pages\EditProfile.php
- A Livewire component's PHP class where all the logic goes. -
resources\filament\pages\edit-profile.blade.php
- a View file where the front-end code goes.
Next, let's change Filament panels settings so the profile link goes to our custom page.
app/Providers/Filament/AdminPanelProvider.php:
use Filament\Navigation\MenuItem;use App\Filament\Pages\EditProfile; class AdminPanelProvider extends PanelProvider{ public function panel(Panel $panel): Panel { return $panel ->default() ->id('admin') ->path('admin') ->login() ->profile() ->userMenuItems([ 'profile' => MenuItem::make()->url(fn (): string => EditProfile::getUrl()) ]) // ... }}
After visiting the Edit Profile page, you should see our custom page with the heading text Edit Profile
.
Showing Multiple Forms on One Page
Next, let's add forms to our custom page. Because the custom page is a Livewire component, we must prepare it to use forms first.
app/Filament/Pages/EditProfile.php:
use Filament\Forms\Contracts\HasForms;use Filament\Forms\Concerns\InteractsWithForms; class EditProfile extends Page implements HasForms{ use InteractsWithForms; protected static string $view = 'filament.pages.edit-profile'; protected static bool $shouldRegisterNavigation = false;}
Now we can add multiple forms, fill one with data, and show them in the browser.
app/Filament/Pages/EditProfile.php:
use Exception;use Filament\Forms;use Filament\Forms\Form;use Filament\Pages\Page;use Filament\Pages\Concerns;use Filament\Actions\Action;use Filament\Facades\Filament;use Illuminate\Support\Facades\Hash;use Illuminate\Database\Eloquent\Model;use Illuminate\Validation\Rules\Password;use Illuminate\Contracts\Auth\Authenticatable; class EditProfile extends Page implements HasForms{ use InteractsWithForms; protected static string $view = 'filament.pages.edit-profile'; protected static bool $shouldRegisterNavigation = false; public ?array $profileData = []; public ?array $passwordData = []; public function mount(): void { $this->fillForms(); } protected function getForms(): array { return [ 'editProfileForm', 'editPasswordForm', ]; } public function editProfileForm(Form $form): Form { return $form ->schema([ Forms\Components\Section::make('Profile Information') ->description('Update your account\'s profile information and email address.') ->schema([ Forms\Components\TextInput::make('name') ->required(), Forms\Components\TextInput::make('email') ->email() ->required() ->unique(ignoreRecord: true), ]), ]) ->model($this->getUser()) ->statePath('profileData'); } public function editPasswordForm(Form $form): Form { return $form ->schema([ Forms\Components\Section::make('Update Password') ->description('Ensure your account is using long, random password to stay secure.') ->schema([ Forms\Components\TextInput::make('Current password') ->password() ->required() ->currentPassword(), Forms\Components\TextInput::make('password') ->password() ->required() ->rule(Password::default()) ->autocomplete('new-password') ->dehydrateStateUsing(fn ($state): string => Hash::make($state)) ->live(debounce: 500) ->same('passwordConfirmation'), Forms\Components\TextInput::make('passwordConfirmation') ->password() ->required() ->dehydrated(false), ]), ]) ->model($this->getUser()) ->statePath('passwordData'); } protected function getUser(): Authenticatable & Model { $user = Filament::auth()->user(); if (! $user instanceof Model) { throw new Exception('The authenticated user object must be an Eloquent model to allow the profile page to update it.'); } return $user; } protected function fillForms(): void { $data = $this->getUser()->attributesToArray(); $this->editProfileForm->fill($data); $this->editPasswordForm->fill(); } }
The main difference in using multiple forms is defining form names in the getForms
method. Then specify an array of public properties for each form and define them in the statePath
when creating each form.
And when calling these forms, we need to call them by their names instead of $this->form
. Names in the getForms
method must be the same as method names.
And now, we can show these forms in the browser.
resources/views/filament/pages/edit-profile.blade.php:
<x-filament-panels::page> <x-filament-panels::form> {{ $this->editProfileForm }} </x-filament-panels::form> <x-filament-panels::form> {{ $this->editPasswordForm }} </x-filament-panels::form></x-filament-panels::page>
After revisiting the Edit Profile page, we see two forms divided into two sections.
Submitting Forms
Before saving data, we need submit buttons. So, let's add them.
app/Filament/Pages/EditProfile.php:
use Filament\Actions\Action; class EditProfile extends Page implements HasForms{ use InteractsWithForms; // ... protected function getUpdateProfileFormActions(): array { return [ Action::make('updateProfileAction') ->label(__('filament-panels::pages/auth/edit-profile.form.actions.save.label')) ->submit('editProfileForm'), ]; } protected function getUpdatePasswordFormActions(): array { return [ Action::make('updatePasswordAction') ->label(__('filament-panels::pages/auth/edit-profile.form.actions.save.label')) ->submit('editPasswordForm'), ]; } // ...}
And we need to call them in the Blade file.
resources/views/filament/pages/edit-profile.blade.php:
<x-filament-panels::page> <x-filament-panels::form wire:submit="updateProfile"> {{ $this->editProfileForm }} <x-filament-panels::form.actions :actions="$this->getUpdateProfileFormActions()" /> </x-filament-panels::form> <x-filament-panels::form wire:submit="updatePassword"> {{ $this->editPasswordForm }} <x-filament-panels::form.actions :actions="$this->getUpdatePasswordFormActions()" /> </x-filament-panels::form> </x-filament-panels::page>
And we have submit buttons for both forms.
Now let's update the data. For this, we need the following methods: updateProfile()
and updatePassword()
.
resources/views/filament/pages/edit-profile.blade.php:
<x-filament-panels::page> <x-filament-panels::form> <x-filament-panels::form wire:submit="updateProfile"> {{ $this->editProfileForm }} <x-filament-panels::form.actions :actions="$this->getUpdateProfileFormActions()" /> </x-filament-panels::form> <x-filament-panels::form> <x-filament-panels::form wire:submit="updatePassword"> {{ $this->editPasswordForm }} <x-filament-panels::form.actions :actions="$this->getUpdatePasswordFormActions()" /> </x-filament-panels::form></x-filament-panels::page>
app/Filament/Pages/EditProfile.php:
use Filament\Support\Exceptions\Halt; class EditProfile extends Page implements HasForms{ // ... public function updateProfile(): void { try { $data = $this->editProfileForm->getState(); $this->handleRecordUpdate($this->getUser(), $data); } catch (Halt $exception) { return; } } public function updatePassword(): void { try { $data = $this->editPasswordForm->getState(); $this->handleRecordUpdate($this->getUser(), $data); } catch (Halt $exception) { return; } if (request()->hasSession() && array_key_exists('password', $data)) { request()->session()->put([ 'password_hash_' . Filament::getAuthGuard() => $data['password'], ]); } $this->editPasswordForm->fill(); } // ... protected function handleRecordUpdate(Model $record, array $data): Model { $record->update($data); return $record; }}
There's nothing magical about updating name
and email
, only updating those two fields in the DB.
For password
, after updating, we must update password_hash_
. Otherwise, the user will get the Route [login] not defined.
error message. And lastly, the form must be reset.
Cool, now a user can update their information or reset their password. But let's also send a success notification.
app/Filament/Pages/EditProfile.php:
use Filament\Notifications\Notification; class EditProfile extends Page implements HasForms{ // ... public function updateProfile(): void { try { $data = $this->editProfileForm->getState(); $this->handleRecordUpdate($this->getUser(), $data); } catch (Halt $exception) { return; } $this->sendSuccessNotification(); } public function updatePassword(): void { try { $data = $this->editPasswordForm->getState(); $this->handleRecordUpdate($this->getUser(), $data); } catch (Halt $exception) { return; } if (request()->hasSession() && array_key_exists('password', $data)) { request()->session()->put([ 'password_hash_' . Filament::getAuthGuard() => $data['password'], ]); } $this->editPasswordForm->fill(); $this->sendSuccessNotification(); } // ... private function sendSuccessNotification(): void { Notification::make() ->success() ->title(__('filament-panels::pages/auth/edit-profile.notifications.saved.title')) ->send(); } }
Bonus: Forms in Jetstream Style?
What if you want the forms styled like the Laravel Jetstream?
It's just one line of code. Add the method ->aside()
!
class EditProfile extends Page implements HasForms{ // ... public function editProfileForm(Form $form): Form { return $form ->schema([ Forms\Components\Section::make('Profile Information') ->aside() ->description('Update your account\'s profile information and email address.') ->schema([ // ... ]), ]) ->model($this->getUser()) ->statePath('profileData'); } public function editPasswordForm(Form $form): Form { return $form ->schema([ Forms\Components\Section::make('Update Password') ->aside() ->description('Ensure your account is using long, random password to stay secure.') ->schema([ // ... ]), ]) ->model($this->getUser()) ->statePath('passwordData'); } // ...}
If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.
thanks. how would you change the session data when changing the information ?
By setting a new value to the session?
is possbile to hide one of the forms based on user role ?
to respond my own question use whatver condition you have for the role in getForms, fillForms and the blade file :)
You can. Simple ifs for che
ok for this one i could not find anything anywhere. how can i send a verification email on email change ?
use eloquent events
Hi, great post!
I'm using the Multi-Tenancy and all is working the same way, just one thing, I don't succeed to call the profile page on a custom route outside the tenant. For now my route is /tenant-1/profile but I'm trying to get /profile as it is not tenant related.
It is working on the default profile page example (even using tenant), but I cannot replicate.
Any idea?
Thank you in advance,
From what I see, it doesn't work on multi-Tenancy :(. Have you found a solution?
There is a property something liie tenant aware which make route to not require a
->requiresConfirmation(true)
? I added it but no effectclear
all fields in that section?We just published a tutorial about Clear button in the form: Filament: Add Custom Button to Form - Reset Example to Clear Fields
How do you implement this if you have multiple panel?
I notice I get filament.{panel-name}.page.{page-name} route not define.
My guess is I need to setup Panel config.
But can't get it to work :|
->userMenuItems([ 'profile' => MenuItem::make()->url(fn (): string => EditProfile::getUrl(panel: 'admin')), ])
As an Admin i have a userResource, when i update the password information i got the message
Route [login] not defined.
as you mentioned in the tutorial i tire to apply this code in EditUser:Hard to tell what you are doing different, but this part is from Filament itself https://github.com/filamentphp/filament/blob/3.x/packages/panels/src/Pages/Auth/EditProfile.php#L138
One way to fix it is to add login route with a redirect
could you give me an example?
What if i want to make two different actions one for edit password and the other for edit the personal infomation (conditionaly hiding the forms), how to acomplish this? any ideas?
You can create a custom page with 2 livewire components inside of it. Each containing individual forms
https://filamentphp.com/docs/3.x/forms/adding-a-form-to-a-livewire-component#using-multiple-forms
It is possible to have a "main filament tab" with multiple forms, each in separate tab? For example: Tab "Profile Information" --> editProfileForm with getUpdateProfileFormActions Tab "Update Password" --> editPasswordForm with getUpdatePasswordFormActions
I think it should be possible using filament tabs blade component https://filamentphp.com/docs/3.x/support/blade-components/tabs
thank you!
If i've multitenancy, how I make profile URL?
There is a package already exists for this
Which one?
he might be talking about companies
How could you add a "relationManager" table, for example "AddressesRelationManager"?
In a resources file, it would look like this:
But how can you do it in a "Page" ?
You would add it manually. How they are added you need to check the source code or search examples in the filament
Thanks.
Where do you import
Halt
from on your exceptions inEditProfile
?Also, I assume the import for the
MenuItem
in theAdminPanelProvider
isuse Filament\Navigation\MenuItem;
use Filament\Support\Exceptions\Halt;
How can I make my two different forms appear under three separate tabs? Where should I place the tabs and the forms associated with each tab?
You can't do it this way. In this case you should create a custom page. With this approach maybe adding a livewire form as a field would work. https://filamentphp.com/docs/3.x/forms/advanced#inserting-livewire-components-into-a-form
In the meantime, I managed to solve it. I'll share it in case someone finds it useful. Thank you.