Visual Editor - v1.0.0
Livewire embedding recipe
The visual editor ships as a self-contained React app, so Livewire can't
render it directly. Instead, mount it inside wire:ignore and bridge
editor activity back to the component with three browser events.
1. Render the editor inside wire:ignore
wire:ignore tells Livewire to leave the mount point alone during DOM
diffs. Without it, Livewire's morph would wipe out the React tree on every
$wire update.
<div>
<div
wire:ignore
@ve:editor:change.window="$wire.set('dirty', true)"
@ve:editor:autosave.window="$wire.handleAutosaved($event.detail)"
@ve:editor:save.window="$wire.handleSaved($event.detail)"
>
<x-visual-editor :model="$post" />
</div>
@if ($flash)
<div class="alert alert-success">{{ $flash }}</div>
@endif
</div>
The @ve:editor:* attributes are Alpine listeners (Alpine ships with
Livewire 3 by default). .window is required — the events are dispatched
on window, not on the editor mount point.
2. Receive the payload in the Livewire component
use Livewire\Attributes\Locked;
use Livewire\Component;
class PostEditor extends Component
{
#[Locked]
public int $postId;
public bool $dirty = false;
public ?string $lastSavedAt = null;
public ?string $flash = null;
/**
* @param array{resource: string, id: string, blocks: array, updatedAt: ?string} $detail
*/
public function handleAutosaved(array $detail): void
{
$this->dirty = false;
$this->lastSavedAt = $detail['updatedAt'];
}
/**
* @param array{resource: string, id: string, blocks: array, updatedAt: ?string} $detail
*/
public function handleSaved(array $detail): void
{
$this->dirty = false;
$this->lastSavedAt = $detail['updatedAt'];
$this->flash = __('Post saved at :time', ['time' => $detail['updatedAt']]);
}
}
Each handler receives $event.detail — the typed CustomEvent payload
documented below. Livewire serializes it as a PHP array, so type-hint with
an array shape.
3. Event contract
Every event is dispatched on window as a CustomEvent. TypeScript hosts
can import { VE_EDITOR_SAVE, type VeEditorSaveDetail } from '@artisanpack-ui/visual-editor/editor' to stay in sync.
| Event | When it fires | detail shape |
|---|---|---|
ve:editor:change |
Debounce window closes, right before the autosave request goes out. | { resource, id, blocks } |
ve:editor:autosave |
Debounce-triggered save returns 200. |
{ resource, id, blocks, updatedAt } |
ve:editor:save |
Explicit save (⌘S / top-bar Save) returns 200. |
{ resource, id, blocks, updatedAt } |
resource and id match the data-resource / data-id attributes the
Blade component emits, so a single listener can disambiguate when multiple
editors are mounted on the same page.
4. Listening in plain JavaScript
Outside Alpine — e.g. a vanilla Blade page with inline script:
<script>
window.addEventListener('ve:editor:save', (event) => {
console.log('saved', event.detail.resource, event.detail.id, event.detail.updatedAt);
});
</script>
Working example
The artisanpack-ui dev app ships a Volt component that wires all three
events to live UI. See
resources/views/packages/visual-editor/m13-livewire-editor.blade.php
(route: /packages/visual-editor/m13/livewire/post/{post}).