Clicking the preview link in the form opens a full-screen modal. The modal contains an editor on the left with a copy of the Builder field, and an iframe on the right to render the preview.
As you edit the Builder blocks, the preview can be refreshed manually or automatically. When the modal is closed, the Builder field in the main form is synchronized with the changes from the preview editor.
Closing the preview modal does not update the record in the database, only the form state is updated.
In your Edit
page, add both HasPreviewModal
and HasBuilderPreview
traits:
use Pboivin\FilamentPeek\Pages\Concerns\HasPreviewModal;
use Pboivin\FilamentPeek\Pages\Concerns\HasBuilderPreview;
class EditPost extends EditRecord
{
use HasPreviewModal;
use HasBuilderPreview;
// ...
Add the getBuilderPreviewView()
method to define your Blade view:
protected function getBuilderPreviewView(string $builderName): ?string
{
// This corresponds to resources/views/posts/preview-blocks.blade.php
return 'posts.preview-blocks';
}
Then, add the getBuilderEditorSchema()
method to define your Builder field:
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Builder;
public static function getBuilderEditorSchema(string $builderName): Component|array
{
return Builder::make('content_blocks')->blocks([
// ...
]);
}
To reduce duplication, the Builder field definition can also be extracted to a static method on the resource class (see Complete Example below).
Add the InlinePreviewAction
action to your form, above or below the Builder field:
use Filament\Forms\Components\Actions;
use Pboivin\FilamentPeek\Forms\Actions\InlinePreviewAction;
Actions::make([
InlinePreviewAction::make()
->label('Preview Content Blocks')
->builderName('content_blocks'),
]),
app/Filament/Resources/PostResource/Pages/EditPost.php
namespace App\Filament\Resources\PostResource\Pages;
use App\Filament\Resources\PostResource;
use Filament\Forms\Components\Component;
use Filament\Resources\Pages\EditRecord;
use Pboivin\FilamentPeek\Pages\Concerns\HasBuilderPreview;
use Pboivin\FilamentPeek\Pages\Concerns\HasPreviewModal;
class EditPost extends EditRecord
{
use HasPreviewModal;
use HasBuilderPreview;
protected static string $resource = PostResource::class;
protected function getBuilderPreviewView(string $builderName): ?string
{
return 'posts.preview-blocks';
}
public static function getBuilderEditorSchema(string $builderName): Component|array
{
return PostResource::contentBuilderField(context: 'preview');
}
}
app/Filament/Resources/PostResource.php
namespace App\Filament\Resources;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Pboivin\FilamentPeek\Forms\Actions\InlinePreviewAction;
// ...
class PostResource extends Resource
{
// ...
public static function contentBuilderField(string $context = 'form'): Builder
{
return Builder::make('content_blocks')->blocks([
Builder\Block::make('heading')->schema([
Grid::make($context === 'preview' ? 1 : 2)->schema([
TextInput::make('title'),
Select::make('level')->options([
'h2' => 'H2',
'h3' => 'H3',
'h4' => 'H4',
])->default('h2'),
Checkbox::make('uppercase')
->columnSpanFull(),
]),
]),
Builder\Block::make('paragraph')->schema([
RichEditor::make('content')
->toolbarButtons(['bold', 'italic']),
]),
])
->columnSpanFull()
->collapsible();
}
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('title')
->columnSpanFull()
->required(),
Actions::make([
InlinePreviewAction::make()
->label('Preview Content Blocks')
->builderName('content_blocks'),
])
->columnSpanFull()
->alignEnd(),
self::contentBuilderField(),
]);
}
// ...
}
For an easy way to try out the plugin on a simple Filament project, have a look at the filament-peek-demo repository.
Most methods in the HasBuilderPreview
trait receive a $builderName
argument. This corresponds to the value defined in the preview action's builderName()
method. Therefore, it's possible to support independent previews for multiple Builder fields in the same form:
protected function getBuilderPreviewView(string $builderName): ?string
{
return match ($builderName) {
'content_blocks' => 'posts.preview-content-blocks',
'footer_blocks' => 'posts.preview-footer-blocks',
};
}
public static function getBuilderEditorSchema(string $builderName): Component|array
{
return match ($builderName) {
'content_blocks' => PostResource::contentBuilderField(context: 'preview'),
'footer_blocks' => PostResource::footerBuilderField(context: 'preview'),
};
}
Filament Tiptap Editor by Adam Weston
You may have noticed that getBuilderEditorSchema()
supports any type of form Component. Behind the scenes, the Editor sidebar of the preview modal is a full Filament form. Therefore, you are not restricted to using a Builder field, you may use any other field type:
use FilamentTiptapEditor\TiptapEditor;
public static function getBuilderEditorSchema(string $builderName): Component|array
{
return TiptapEditor::make('post_content');
}
Using a single field should work without any other modifications. To support multiple fields in the sidebar, consider using a Group
component with a custom state path:
public static function getBuilderEditorSchema(string $builderName): Component|array
{
return Group::make([
TextInput::make('title'),
TextInput::make('tagline'),
TiptapEditor::make('body'),
// ...
])->statePath('post_content');
}
This feature was initially designed with a focus on the Builder field, using Blocks composed of built-in Filament fields. It's possible to integrate with custom field types and 3rd party plugins but obviously, not all combinations have been tested.
Please feel free to report any issues you encounter with custom fields in this repository. I can work with you to determine where the issue is coming from.
By default, the action is styled as a primary link. Use the button()
method to style it as a Filament button.
Use one of the following methods on the Actions
wrapper to adjust the horizontal alignment:
alignStart()
alignCenter()
alignEnd()
alignJustify()
By default, the Editor sidebar is not reactive: updating the fields won't automatically refresh the preview iframe. Use the canEnableAutoRefresh
option in the configuration to add a checkbox in the header of the sidebar. The checkbox lets users opt into the auto-refresh behavior.
Additionally, you may choose between two auto-refresh strategies with the autoRefreshStrategy
option:
Name | Description |
---|---|
simple |
The default strategy, which makes all fields in the sidebar behave as lazy() , without any other configuration. The preview modal is refreshed automatically each time the focus is taken out of a field (e.g. pressing the Tab key or clicking away). Because the preview iframe renders a full Blade view, this is a good compromise between user experience and performance. |
reactive |
The alternative strategy, which lets you make fields lazy() or reactive() as needed. Any field not explicitly configured as lazy or reactive will not trigger a refresh. |
Important: Making all fields reactive will have a significant performance penalty and add unnecessary strain on your Web server. Consider using debounce()
in addition to reactive()
on your form fields.
When the Builder preview modal opens, the Editor sidebar is initialized with the Builder data from the main form. Use the mutateInitialBuilderEditorData()
method to interact with the data once, before opening the preview modal:
public function mutateInitialBuilderEditorData(string $builderName, array $editorData): array
{
$editorData['preview_started_at'] = now();
return $editorData;
}
Let's say that your Builder field is named content
. By default, a $content
variable is made available to the rendered Blade view. Use the mutateBuilderPreviewData()
method to interact with the Builder preview data each time, before the preview is refreshed:
public static function mutateBuilderPreviewData(string $builderName, array $editorData, array $previewData): array
{
$previewData['message'] = "This is a preview. It started at {$editorData['preview_started_at']}.";
return $previewData;
}
This would make a $message
variable available to the Blade view when rendered in the iframe.
If you're not using Blade views on the front-end, override the renderBuilderPreview()
method and render the preview with your solution of choice:
public static function renderBuilderPreview(string $view, array $data): string
{
return MyTemplateEngine::render($view, $data);
}
Documentation