Tricks

Template based forms

Nov 7, 2022
Dennis Koch
Admin panel

Introduction

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.

Implementation

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

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

Showing fields based on the selected template

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.

Loading and saving 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;
}

Closing remarks

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…