Skip to content

Visual Editing Setup

Visual editing allows authorized users to click on frontend content and edit it inline. This guide shows you how to set it up for your sections.

  1. Install the visual editing package:

    Terminal window
    composer require franc014/ve-filament-cms-livewire
  2. Run the installer:

    Terminal window
    php artisan ve-filament-cms-livewire:install
graph TD
    A[User clicks Edit button] -->|toggle| B[EditingMode activates]
    B -->|page reload| C[DOM instruments data-cms-source]
    C -->|user clicks element| D[VisualEditor opens modal]
    D -->|loads| E[InlineEditForm]
    E -->|saves| F[Database updates]
    F -->|emits| G[contentUpdated event]
    G -->|refreshes| H[Livewire Component]

Add InteractsWithVisualEditing from ve-filament-cms-livewire:

namespace App\Livewire;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent
{
use InteractsWithVisualEditing;
public string $label = '';
public string $headline = '';
public array $cmsSourceMap = [];
// autoHydrate() handles $label and $headline automatically
protected function initializeVisualEditing(): void
{
$block = $this->block?->first();
if ($block !== null) {
$this->cmsSourceMap = $block->getSourceMap();
}
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.components.hero');
}
}

Note: Without this trait, renderField() is unavailable. Non-visual-editing components simply don’t need it — use {{ $label }} directly.

renderField() is provided by the InteractsWithVisualEditing trait. When visual editing is active, it injects data-cms-source attributes automatically:

{{-- Before --}}
<span>{{ $label }}</span>
<h1>{{ $headline }}</h1>
{{-- After --}}
<span>{!! $this->renderField('label', 'text') !!}</span>
<h1>{!! $this->renderField('headline', 'text') !!}</h1>

Pass the appropriate field type to render the correct editor:

TypeEditorUse For
textTextInputShort text, headlines
textareaTextareaMedium text, descriptions
rich_textRichEditorFormatted content
imageFileUploadImages
repeaterRepeaterLists, grids
<img
src="{{ $imageUrl }}"
alt="{{ $imageAlt }}"
data-cms-source="{{ $this->renderImageField('image_url') }}"
>
<div data-cms-source="{{ $this->renderRepeaterContainer('members') }}">
@foreach($members as $member)
<div>{{ $member['name'] }}</div>
@endforeach
</div>

Add the visual editor component to your main layout:

{{-- resources/views/layouts/app.blade.php --}}
<body>
{{ $slot }}
@auth
@can('EditFrontendContent')
<livewire:visual-editor />
@endcan
@endauth
</body>

Or include it unconditionally (the component checks authorization internally):

<livewire:visual-editor />
  1. Log in as a user with EditFrontendContent permission
  2. Visit a page with your section
  3. Click the floating “Edit” button
  4. Click on editable content
  5. Edit and save
  6. Verify changes appear immediately

Labels are automatically extracted from your ContentBlock schema — no manual fieldLabels() method needed:

// In your ContentBlock:
Block::make('hero')
->schema([
TextInput::make('headline')
->label('Headline') // This label is auto-extracted
->required(),
])
// BlockSchemaExtractor reads 'Headline' from the schema
// renderField() uses it for visual editing tooltips

For localized labels, use translation files with nested arrays:

lang/es/cms.php
return [
'hero' => [
'fields' => [
'label' => 'Etiqueta',
'headline' => 'Titular',
],
],
];
// In your ContentBlock:
TextInput::make('headline')
->label(__('cms.hero.fields.headline'))

Important: Repeater parent keys MUST use .label suffix:

Repeater::make('key_points')
->label(__('cms.mission.fields.key_points.label')) // .label suffix!
->schema([
TextInput::make('title')
->label(__('cms.mission.fields.key_points.title')),
])

Session-backed singleton:

$editingMode = app(\JFA\VeFilamentCMSLivewire\EditingMode::class);
$editingMode->activate(); // Enable editing
$editingMode->deactivate(); // Disable editing
$editingMode->isActive(); // Check state

Auto-registered by VeFilamentCMSLivewireServiceProvider on the web group:

// Ensures EditingMode is resolved early
public function handle(Request $request, Closure $next): Response
{
$this->editingMode->isActive();
return $next($request);
}

When content is saved:

// InlineEditForm dispatches
dispatch('contentUpdated', sectionId: $sectionId);
// InteractsWithVisualEditing trait listens automatically
// refreshFromCMS() re-hydrates the section from the database
sequenceDiagram
    participant VE as VisualEditor
    participant IEF as InlineEditForm
    participant DB as Database
    participant SC as SectionComponent
    
    VE->>IEF: Save changes
    IEF->>DB: Update section content
    IEF->>SC: dispatch('contentUpdated')
    SC->>DB: Section::find(sectionId)
    DB->>SC: Updated section
    SC->>SC: resolve() → BlockCollection
    SC->>SC: autoHydrate()
    SC->>SC: Re-render with new data

You can disable visual editing via config without uninstalling the package:

config/filament-cms-livewire.php
'visual_editing' => [
'enabled' => false,
],

Or via environment variable:

VISUAL_EDITING_ENABLED=false

When disabled, renderField() outputs plain values and the contentUpdated listener is a no-op.

Use a dynamic wire:key to force Livewire remount when switching fields:

<livewire:inline-edit-form
:source-map="$activeSourceMap"
wire:key="inline-edit-{{ md5(json_encode($activeSourceMap)) }}"
/>

This prevents form schema caching bugs where the wrong field label or type is shown.

ProblemCauseSolution
Edit button not showingMissing permissionAssign EditFrontendContent role
Elements not clickableMissing renderField()Use {!! $this->renderField() !!}
Wrong field type shownMissing field_typePass second parameter to renderField()
Changes not reflectingMissing source mapInitialize $this->cmsSourceMap in initializeVisualEditing()
Labels showing as keysMissing translationsAdd to lang/{locale}/cms.php
Modal not openingMissing wire:keyAdd dynamic wire:key on inline-edit-form
Content disappears after saveLivewire serializationDon’t override mount() without calling parent::mount()