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]

Step 1: Add the Trait and Implement resolveFieldValue()

Section titled “Step 1: Add the Trait and Implement resolveFieldValue()”

Add InteractsWithVisualEditing from ve-filament-cms-livewire and implement resolveFieldValue():

namespace App\Livewire;
use JFA\FilamentCMSCore\CMS\SectionContent;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent
{
use InteractsWithVisualEditing;
public string $label = '';
public string $headline = '';
protected function hydrateFromContent(SectionContent $content): void
{
$this->label = $content->hero->label ?? '';
$this->headline = $content->hero->headline ?? '';
}
protected function resolveFieldValue(string $field): mixed
{
return match ($field) {
'label' => $this->label,
'headline' => $this->headline,
default => null,
};
}
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.

To enable inline editing, populate $cmsSourceMap during initialization:

namespace App\Livewire;
use JFA\FilamentCMSCore\CMS\SectionContent;
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 = [];
protected function hydrateFromContent(SectionContent $content): void
{
$this->label = $content->hero->label ?? '';
$this->headline = $content->hero->headline ?? '';
}
protected function initializeVisualEditing(): void
{
$sourceMap = $this->sectionContent->hero->getSourceMap();
if ($sourceMap !== null) {
$this->cmsSourceMap = $sourceMap;
}
}
protected function resolveFieldValue(string $field): mixed
{
return match ($field) {
'label' => $this->label,
'headline' => $this->headline,
default => null,
};
}
protected function getCmsSourceMap(): array
{
return $this->cmsSourceMap;
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.components.hero');
}
}

The initializeVisualEditing() hook is called automatically by SectionComponent::mount() after hydrateFromContent().

Step 3: Implement HasFieldLabels (Optional)

Section titled “Step 3: Implement HasFieldLabels (Optional)”

For localized field labels in the editor:

namespace App\Livewire;
use JFA\FilamentCMSCore\CMS\SectionContent;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
use JFA\VeFilamentCMSLivewire\Contracts\HasFieldLabels;
class Hero extends SectionComponent implements HasFieldLabels
{
use InteractsWithVisualEditing;
public string $label = '';
public string $headline = '';
public array $cmsSourceMap = [];
protected function hydrateFromContent(SectionContent $content): void
{
$this->label = $content->hero->label ?? '';
$this->headline = $content->hero->headline ?? '';
}
protected function initializeVisualEditing(): void
{
$sourceMap = $this->sectionContent->hero->getSourceMap();
if ($sourceMap !== null) {
$this->cmsSourceMap = $sourceMap;
}
}
protected function resolveFieldValue(string $field): mixed
{
return match ($field) {
'label' => $this->label,
'headline' => $this->headline,
default => null,
};
}
protected function getCmsSourceMap(): array
{
return $this->cmsSourceMap;
}
public function fieldLabels(): array
{
return [
'label' => __('cms.hero.fields.label'),
'headline' => __('cms.hero.fields.headline'),
'primary_cta_text' => __('cms.hero.fields.primary_cta_text'),
];
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.components.hero');
}
}

Create translations in lang/es/cms.php:

return [
'hero' => [
'fields' => [
'label' => 'Etiqueta',
'headline' => 'Titular',
'primary_cta_text' => 'Texto del CTA',
],
],
];

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

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);
// SectionComponent listens (via InteractsWithVisualEditing trait)
public function refreshFromCMS(int $sectionId): void
{
if (! $this->isVisualEditingActive()) {
return;
}
if (($this->getCmsSourceMap()['section_id'] ?? null) !== $sectionId) {
return;
}
$section = \JFA\FilamentCMSCore\Models\Section::find($sectionId);
$resolver = new \JFA\FilamentCMSCore\CMS\ContentResolver($section);
$content = $resolver->resolve();
$this->hydrateFromContent($content);
}

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.

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
Labels showing as keysMissing translationsAdd to lang/{locale}/cms.php
Modal not openingMissing wire:keyAdd dynamic wire:key on inline-edit-form