If you come from a traditional CMS like WordPress, you are probably used to the concept of Advanced Custom Fields combined with templates. You want to give your users some options about the design of their page, but don't let them go crazy. A good compromise is to use fixed templates the user can choose from to lay out the frame of the page. Usually each template requires different content, ergo different fields.
This trick will show you an approach to display fields regarding to a selected template.
We start with a Page
resource that represents the various subpages in our frontend. First we need a Select
component to choose our template from.
return $form ->schema([ Forms\Components\Select::make('template') ->reactive() ->options(static::getTemplates()), ]);
We use static::getTemplates()
to dynamically generate that template list later.
Templates are simple classes that a list of fields and have a name. I put them inside App\Filament\PageTemplates
but you can place them wherever you want. A template may look like this:
<?php namespace App\Filament\PageTemplates; use Filament\Forms\Components\Repeater;use Filament\Forms\Components\RichEditor;use Filament\Forms\Components\TextInput; final class Faq{ public static function title() { return 'FAQ'; } public static function schema() { return [ TextInput::make('title'), Repeater::make('faq')->label('FAQ')->schema([ TextInput::make('title') Repeater::make('items')->schema([ TextInput::make( 'title'), RichEditor::make('content') ]) ]) ]; }}
We then scan our template folder for all files ...
public static function getTemplateClasses(): Collection{ $filesystem = app(Filesystem::class); return collect($filesystem->allFiles(app_path('Filament/PageTemplates'))) ->map(function (SplFileInfo $file): string { return (string) Str::of('App\\Filament\\PageTemplates') ->append('\\', $file->getRelativePathname()) ->replace(['/', '.php'], ['\\', '']); });}
... and create the options for the initial template Select
field from this.
public static function getTemplates(): Collection{ return static::getTemplateClasses()->mapWithKeys(fn ($class) => [$class => $class::title()]);}
To show dynamically update the fields based on our selected template we need to update our initial form schema. We introduce a helper function getTemplateSchemas()
that retrieves all schemas from the template classes:
return $form ->schema([ Forms\Components\Select::make('template') ->reactive() ->options(static::getTemplates()), ...static::getTemplateSchemas(), ]);
public static function getTemplateSchemas(): array{ return static::getTemplateClasses() ->map(fn ($class) => Forms\Components\Group::make($class::schema()) ->columnSpan(2) ->afterStateHydrated(fn ($component, $state) => $component->getChildComponentContainer()->fill($state)) ->statePath('temp_content.' . static::getTemplateName($class)) ->visible(fn ($get) => $get('template') === $class) ) ->toArray();}
Note that we use a separate Group
with a unique ->statePath()
for every template. This prevents data from colliding when you switch templates. For example, if you have a content field that is a Textarea
in one template but a Repeater
in another template. The ->afterStateHydrated()
call makes sure the group is filled with the correct defaults and data.
So far we have our Page
resource that shows different fields based on the selected template. As a last step we also need to fill the fields with data for existing Pages and store that data in the database. I assume that you have a pages
table with a content
column that is casted as JSON.
We use the mutateFormDataBeforeFill()
lifecycle hook to prepare the data for our given form schema:
protected function mutateFormDataBeforeFill(array $data): array{ $data['temp_content'][static::getTemplateName($data['template'])] = $data['content']; unset($data['content']); return $data;}
Using mutateFormDataBeforeSave()
we make sure the right one of our temporary data arrays gets saved:
protected function mutateFormDataBeforeSave(array $data): array{ $data['content'] = $data['temp_content'][static::getTemplateName($data['template'])]; unset($data['temp_content']); return $data;}
That's about it. You can find the full code in this gist: https://gist.github.com/pxlrbt/15342387355aeae0c1b043ab385be8a8.
Hope you found this useful. If you have any questions shoot me a message on the Filament Discord @pxlrbt.
No comments yet…