Filament Custom Edit Profile Page: Multiple Forms and Full-Page Design

Filament Custom Edit Profile Page: Multiple Forms and Full-Page Design
Admin
Sunday, August 27, 2023 7 mins to read
Share
Filament Custom Edit Profile Page: Multiple Forms and Full-Page Design

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:

default filament profile page

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:

breeze profile page

So, we have two goals for this tutorial:

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.

empty custom profile page


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.

multiple forms without action buttons


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.

multiple forms with action buttons

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();
}
}

success notification


Bonus: Forms in Jetstream Style?

What if you want the forms styled like the Laravel Jetstream?

laravel jetstream style

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');
}
 
// ...
}

custom profile jetstream style


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