Components

Notifications and Actions

How Tagixo builder events map to Primix notifications and LiVue dispatch patterns.

Notifications & Actions in Primix Visual Builder Pages

This document covers how Primix surfaces notifications to the user, how builder-canvas events flow between the Vue iframe and PHP/Livewire, and how to wire up header actions on visual builder pages.


The notification bridge

InteractsWithVisualBuilderPrimix (the Primix trait used by visual builder pages) implements two notification helpers:

use Primix\Notifications\Notification; // ← correct namespace

protected function notifySuccess(string $message): void
{
    Notification::make()
        ->title($message)
        ->success()
        ->send();
}

protected function notifyError(string $message): void
{
    Notification::make()
        ->title($message)
        ->danger()
        ->send();
}

Warning: do not import Filament\Notifications\Notification. Primix ships its own notification class. Mixing the two namespaces causes a silent render failure.


Save success: the saveFromVue() flow

When the user clicks Save inside the builder canvas, the canvas emits a tagixo:save window event. The Primix bridge script intercepts it and calls livue.call('saveFromVue'), which reaches the PHP method of the same name on the page component:

// Inside your PrimixVisualBuilderPage subclass
public function saveFromVue(array $payload): void
{
    try {
        $this->saveStructure($payload);
        $this->notifySuccess('Page saved.');
        $this->dispatch('tagixo:saved');         // tells the canvas save succeeded
    } catch (\Exception $e) {
        $this->notifyError('Save failed: ' . $e->getMessage());
        $this->dispatch('tagixo:save-error');    // tells the canvas save failed
        // do NOT re-throw — the catch block is the terminal handler here
    }
}

protected function saveStructure(array $payload): void
{
    $page = $this->getRecord();
    $page->structure = $payload['structure'] ?? [];
    $page->save();

    // Re-render so the preview reflects the new content
    PageRenderer::clearCache($page);
}

The exception is routed to tagixo:save-error rather than propagated — this prevents a 500 page from replacing the builder UI mid-session.


LiVue dispatch vs Livewire

Primix visual builder pages use LiVue dispatch for canvas↔PHP communication, not raw Livewire events. The two mechanisms differ:

Concern LiVue (livue.call / livue.on) Livewire (wire:click / #[On])
Crosses iframe boundary Yes — LiVue bridge handles postMessage No — Livewire expects same-document DOM
PHP listener attribute #[On('livue:methodName')] (auto-routed) #[On('eventName')]
JS emit window.livue.call('methodName', payload) $wire.call('methodName', payload)
JS listen window.livue.on('eventName', cb) $wire.on('eventName', cb)

PHP side — listening to a LiVue call:

// Method name IS the event — no attribute needed for livue.call()
public function saveFromVue(array $payload): void { ... }

// For fire-and-forget events emitted by the canvas:
#[On('tagixo:requestPreview')]
public function handlePreviewRequest(): void { ... }

JS side — calling PHP from inside (or outside) the canvas:

// Call a PHP method with a payload
window.livue.call('saveFromVue', { structure: [...] });

// Listen to a PHP-dispatched event
window.livue.on('tagixo:saved', () => console.log('saved'));

Builder canvas events

Events emitted by the canvas (JS → PHP)

Event Trigger Payload
tagixo:save User clicks Save in builder { structure, meta }
tagixo:requestPreview User clicks Preview in builder
tagixo:dirty Unsaved changes detected { dirty: bool }

Events listened to by the canvas (PHP → JS)

Event Effect
tagixo:saved Clears dirty flag, shows in-canvas "Saved" toast
tagixo:save-error Re-enables Save button, shows in-canvas error
tagixo:reload Full canvas reload (use after publish/unpublish)

The bridge script inside builder-vue.blade.php wires these together:

// tagixo:save  →  livue.call('saveFromVue')  →  tagixo:saved / tagixo:save-error
window.addEventListener('tagixo:save', (e) => {
    window.livue.call('saveFromVue', e.detail)
        .then(() => window.dispatchEvent(new Event('tagixo:saved')))
        .catch(() => window.dispatchEvent(new Event('tagixo:save-error')));
});

Attaching your own JS listener for canvas events:

window.addEventListener('tagixo:saved', () => {
    // e.g. update a "Last saved" timestamp in the page chrome
});

Header actions on builder pages

Override getHeaderActions() on your page class to add Primix Action instances to the top bar:

use Primix\Pages\Actions\Action;

protected function getHeaderActions(): array
{
    return [
        Action::make('preview')
            ->label('Preview')
            ->icon('heroicon-o-eye')
            ->url(fn () => route('pages.show', $this->getRecord()->slug))
            ->openUrlInNewTab(),

        Action::make('publish')
            ->label('Publish')
            ->icon('heroicon-o-globe-alt')
            ->color('success')
            ->action(function () {
                $this->getRecord()->update(['published_at' => now()]);
                $this->notifySuccess('Page published.');
            }),
    ];
}

Common Action methods:

Method Purpose
->label(string) Button label
->icon(string) Heroicon name
->color(string) primary / success / danger / warning / gray
->action(Closure) PHP callback executed on click
->url(string|Closure) Navigate instead of calling PHP
->openUrlInNewTab() Opens ->url() in a new browser tab
->visible(Closure) Conditionally show/hide the action
->disabled(Closure) Render but disable the button
->requiresConfirmation() Show a modal before executing

Confirmation before destructive actions

Use ->requiresConfirmation() to gate dangerous actions behind a modal:

Action::make('resetContent')
    ->label('Reset content')
    ->icon('heroicon-o-arrow-path')
    ->color('danger')
    ->requiresConfirmation()
    ->modalHeading('Reset page content?')
    ->modalDescription('This will discard all builder content and cannot be undone.')
    ->modalSubmitActionLabel('Yes, reset')
    ->action(function () {
        $this->getRecord()->update(['structure' => []]);
        $this->dispatch('tagixo:reload');   // force canvas to reload blank state
        $this->notifySuccess('Content reset.');
    }),

The modal is rendered by Primix's own dialog component — no additional Blade markup required.


Unsaved changes in LiVue context

The builder canvas registers its own beforeunload guard whenever dirty === true. This covers hard browser navigations (closing tab, typing a new URL).

For in-app SPA navigations (clicking a sidebar link), Primix exposes a beforeLeave() hook on the page component:

public function beforeLeave(): bool
{
    // Return false to block navigation and show an "Unsaved changes" prompt.
    // The canvas emits 'tagixo:dirty' which you can track in a component property.
    return ! $this->isDirty;
}

Track dirty state by listening to the canvas event:

public bool $isDirty = false;

#[On('tagixo:dirty')]
public function setDirty(bool $dirty): void
{
    $this->isDirty = $dirty;
}

Complete example

A full PrimixVisualBuilderPage with Preview, Publish, and Unpublish header actions:

<?php

namespace App\Primix\Pages;

use App\Models\Page;
use Ccast\Tagixo\Services\PageRenderer;
use Primix\Pages\Actions\Action;
use Primix\Pages\PrimixVisualBuilderPage;

class EditPage extends PrimixVisualBuilderPage
{
    protected static string $model = Page::class;

    public bool $isDirty = false;

    // ── Canvas events ────────────────────────────────────────────────────────

    public function saveFromVue(array $payload): void
    {
        try {
            $page = $this->getRecord();
            $page->structure = $payload['structure'] ?? [];
            $page->save();
            PageRenderer::clearCache($page);

            $this->notifySuccess('Page saved.');
            $this->dispatch('tagixo:saved');
        } catch (\Exception $e) {
            $this->notifyError('Save failed: ' . $e->getMessage());
            $this->dispatch('tagixo:save-error');
        }
    }

    #[On('tagixo:dirty')]
    public function setDirty(bool $dirty): void
    {
        $this->isDirty = $dirty;
    }

    // ── Navigation guard ─────────────────────────────────────────────────────

    public function beforeLeave(): bool
    {
        return ! $this->isDirty;
    }

    // ── Header actions ───────────────────────────────────────────────────────

    protected function getHeaderActions(): array
    {
        return [
            Action::make('preview')
                ->label('Preview')
                ->icon('heroicon-o-eye')
                ->url(fn () => route('pages.show', $this->getRecord()->slug))
                ->openUrlInNewTab(),

            Action::make('unpublish')
                ->label('Unpublish')
                ->icon('heroicon-o-eye-slash')
                ->color('warning')
                ->visible(fn () => $this->getRecord()->isPublished())
                ->requiresConfirmation()
                ->modalHeading('Unpublish this page?')
                ->modalDescription('The page will be immediately removed from the public site.')
                ->modalSubmitActionLabel('Unpublish')
                ->action(function () {
                    $this->getRecord()->update(['published_at' => null]);
                    $this->notifySuccess('Page unpublished.');
                }),

            Action::make('publish')
                ->label('Publish')
                ->icon('heroicon-o-globe-alt')
                ->color('success')
                ->visible(fn () => ! $this->getRecord()->isPublished())
                ->action(function () {
                    $this->getRecord()->update(['published_at' => now()]);
                    $this->notifySuccess('Page published.');
                }),
        ];
    }
}