The final lesson in this mini-course is about ready-made examples. Need inspiration for customizing the forms? Let's take a look at a few open-source projects.
1. Plot/Roadmap: Tabs & Sections
The first example is from an open-source project ploi/roadmap.
Here, we have a Settings page where different settings are separated into tabs.
class Settings extends SettingsPage{ // ... public function form(Form $form): Form { return $form->schema( [ Tabs::make('main') ->persistTabInQueryString() ->schema( [ Tabs\Tab::make(trans('settings.general-title')) ->schema( [ Section::make('') ->columns() ->schema( [ // Regular form fields in the section ] ), // Other form fields ] ), Tabs\Tab::make(trans('settings.default-boards-title')) ->schema( [ // Regular form fields ] ) ->columnSpan(2) ->visible(fn (Get $get) => $get('create_default_boards')), ] ), Tabs\Tab::make(trans('settings.dashboard-items-title')) ->schema( [ // Regular form fields ] ), Tabs\Tab::make(trans('settings.changelog-title')) ->schema( [ // Regular form fields ] ), Tabs\Tab::make(trans('settings.notifications-title')) ->schema( [ // Regular form fields ] ), Tabs\Tab::make(trans('settings.scripts-title')) ->schema( [ // Regular form fields ] ), Tabs\Tab::make(trans('settings.search-title')) ->schema( [ // Regular form fields ] ), Tabs\Tab::make(trans('settings.profanity-title')) ->schema( [ // Regular form fields ) ] ) ->columns() ->columnSpan(2), ] ); } // ...}
2. Hasnayeen/invobook: Tabs, Section, and Fieldset
The second example is from an open-source project Hasnayeen/invobook. Here, we have a custom page for the user's profile. Each tab has its own form.
The page class with defined three forms looks like this:
class Profile extends Page implements HasForms{ // ... protected function getForms(): array { return [ 'detailsForm', 'updatePasswordForm', 'rateForm', ]; } public function detailsForm(Form $form): Form { return $form ->schema([ Section::make(['Details']) ->schema([ Fieldset::make('Personal Information') ->columns(2) ->schema([ TextInput::make('name') ->autofocus() ->required(), TextInput::make('email') ->email() ->required() ->columnStart(1), ]), ]), ]) ->statePath('detailsData') ->model($this->user); } public function updatePasswordForm(Form $form): Form { return $form ->schema([ Section::make(['Details']) ->schema([ Fieldset::make('Update Password') ->columns(2) ->schema([ TextInput::make('current_password') ->password(), TextInput::make('new_password') ->password() ->autocomplete('new-password') ->columnStart(1), TextInput::make('password_confirmation') ->password() ->autocomplete('new-password') ->columnStart(1), ]), ]), ]) ->statePath('detailsData') ->model(auth()->user()); } public function rateForm(Form $form): Form { return $form ->schema([ Section::make(['Details']) ->schema([ Fieldset::make('Default Rate') ->columns(2) ->schema([ TextInput::make('default.amount_in_cents') ->label('Amount') ->integer() ->requiredWith('default.currency') ->formatStateUsing(fn ($state) => $state / 100) ->dehydrateStateUsing(fn ($state) => $state * 100), Select::make('default.currency') ->label('Currency') ->requiredWith('default.amount_in_cents') ->options(ISOCurrencyProvider::getInstance()->getAvailableCurrencies()) ->columnStart(1), ]), Fieldset::make('Rate for this team') ->columns(2) ->schema([ TextInput::make('team.amount_in_cents') ->label('Amount') ->integer() ->requiredWith('team.currency') ->formatStateUsing(fn ($state) => $state / 100) ->dehydrateStateUsing(fn ($state) => $state * 100), Select::make('team.currency') ->label('Currency') ->requiredWith('team.amount_in_cents') ->options(ISOCurrencyProvider::getInstance()->getAvailableCurrencies()) ->columnStart(1), ]), ]), ]) ->statePath('rateData') ->model(auth()->user()); } // ...}
Then, because this is a custom page, tabs are added manually using the Tabs Blade component. When a tab is active, the corresponding form is shown.
<x-filament::page> <div x-data="{ activeTab: 'detailsForm' }" class="space-y-6"> <x-filament::tabs label="Content tabs" contained> <x-filament::tabs.item icon="lucide-file-text" alpine-active="activeTab === 'detailsForm'" x-on:click="activeTab = 'detailsForm'" > {{ __('Details') }} </x-filament::tabs.item> <x-filament::tabs.item icon="lucide-lock" alpine-active="activeTab === 'updatePasswordForm'" x-on:click="activeTab = 'updatePasswordForm'" > {{ __('Update Password') }} </x-filament::tabs.item> <x-filament::tabs.item icon="lucide-banknote" alpine-active="activeTab === 'rateForm'" x-on:click="activeTab = 'rateForm'" > {{ __('Rate') }} </x-filament::tabs.item> </x-filament::tabs> <form x-ref="detailsForm" :class="activeTab === 'detailsForm' || 'hidden'" class="space-y-6" wire:submit="saveDetails"> {{ $this->detailsForm }} <x-filament::button type="submit"> {{ __('Save') }} </x-filament::button> </form> <form x-ref="updatePasswordForm" :class="activeTab === 'updatePasswordForm' || 'hidden'" class="space-y-6" wire:submit="savePassword"> {{ $this->updatePasswordForm }} <x-filament::button type="submit"> {{ __('Save') }} </x-filament::button> </form> <form x-ref="rateForm" :class="activeTab === 'rateForm' || 'hidden'" class="space-y-6" wire:submit="saveRates"> {{ $this->rateForm }} <x-filament::button type="submit"> {{ __('Save') }} </x-filament::button> </form> </div> <x-filament-actions::modals /></x-filament::page>
3. Filament Examples CMS: Group & Section
The third example is from our own FilamentExamples. In this example, we have two groups. The first group is on the left and wider. Everything in the first group is inside a section. The second group is on the right and contains two sections.
class PostResource extends Resource{ // ... public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Group::make() ->schema([ Forms\Components\Section::make() ->schema([ Forms\Components\TextInput::make('title') ->required() ->live(onBlur: true) ->afterStateUpdated(fn(Set $set, ?string $state) => $set('slug', Str::slug($state))), Forms\Components\TextInput::make('slug') ->required(), Forms\Components\RichEditor::make('content') ->live(onBlur: true) ->required(), Forms\Components\Textarea::make('excerpt') ->required(), Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('Generate excerpt') ->action(function (Forms\Get $get, Set $set) { $set('excerpt', str($get('content'))->stripTags()->words(45, end: '')); }) ->size(ActionSize::ExtraSmall) ]), Forms\Components\Select::make('tags') ->multiple() ->relationship('tags', 'name'), Forms\Components\Select::make('category_id') ->relationship('category', 'name') ->required(), ])->columns(1), ])->columnSpan(2), Forms\Components\Group::make() ->schema([ Forms\Components\Section::make('Featured Image') ->schema([ Forms\Components\SpatieMediaLibraryFileUpload::make('featured_image') ->live() ->image() ->hiddenLabel() ->collection('featured_image') ->rules(Rule::dimensions()->maxWidth(600)->maxHeight(800)) ->afterStateUpdated(function (Forms\Contracts\HasForms $livewire, Forms\Components\SpatieMediaLibraryFileUpload $component) { $livewire->validateOnly($component->getStatePath()); }), ]), Forms\Components\Section::make() ->schema([ Forms\Components\Select::make('author_id') ->label('Author') ->relationship('author', 'name') ->required(), Forms\Components\DateTimePicker::make('published_at') ->default(now()), ]), ])->columnSpan(1), ])->columns(3); } // ...}
4. Frikishaan/tiny-crm: Section
The last example is from an open-source project frikishaan/tiny-crm. Similar to the previous example, here we have two sections where one is wider.
class DealResource extends Resource{ // ... public static function form(Form $form): Form { return $form ->schema([ Section::make() ->schema([ TextInput::make('title') ->required() ->disabled(fn(?Deal $record) => in_array($record?->status, [2, 3])), Select::make('customer_id') ->label('Customer') ->options(Account::all()->pluck('name', 'id')) ->searchable() ->disabled(fn(?Deal $record) => in_array($record?->status, [2, 3])) ->required(), Select::make('lead_id') ->label('Originating lead') ->options(Lead::all()->pluck('title', 'id')) ->searchable() ->disabled(fn(?Deal $record) => in_array($record?->status, [2, 3])), RichEditor::make('description') ->disableToolbarButtons([ 'attachFiles', 'codeBlock' ]) ]) ->columnSpan(2), Section::make() ->schema([ Select::make('status') ->options([ 1 => 'Open', 2 => 'Won', 3 => 'Lost' ]) ->visible(fn(?Deal $record) => $record != null) ->disabled(), TextInput::make('estimated_revenue') ->label('Estimated revenue') ->mask(RawJs::make('$money($input)')) ->stripCharacters(',') ->numeric() ->disabled(fn(?Deal $record) => in_array($record?->status, [2, 3])), TextInput::make('actual_revenue') ->label('Actual revenue') ->mask(RawJs::make('$money($input)')) ->stripCharacters(',') ->numeric() ->disabled(fn(?Deal $record) => in_array($record?->status, [2, 3])) ]) ->columnSpan(1) ]) ->columns(3); } // ...}
I finished the course but my progress is 0
Hi, sorry about that - we were doing some optimizations and there was a tiny issue. Should be fixed now!
I was wondering if you have done or could do an example where the Top Navigation is placed in a header instead of the topbar. I am trying to make a layout where I have:
Topbar - site branding and user identification or join/login buttons
Header - main page navigation
Main - content and secondary navigation
Filament will only put main navigation in a sidebar or the topbar. I tried creating a custom layout but don't understand their code well enough to pull it off.
We haven't done such but the logic would be use render hooks and add filament components to them. Which hook to use check docs. For which component to render only diving into source code you would find. Just today on X someone shared some modifications, might give you some directions.