v1 LiVue v1 is here — server-driven reactivity for Laravel using Vue.js Get Started →

Extending LiVue

LiVue is designed to be extensible on both the JavaScript and PHP sides. Register Vue plugins, add lifecycle hooks, create custom PHP features, and manage assets for packages.

Vue Plugins & Components

Use LiVue.setup() to hook into the Vue app creation process. The callback receives the Vue app instance before it mounts, so you can register plugins, global components, and directives.

resources/js/app.js
import LiVue from 'livue';
import { createVuetify } from 'vuetify';
import MyButton from './components/MyButton.vue';

const vuetify = createVuetify({ /* ... */ });

LiVue.setup((app) => {
    app.use(vuetify);
    app.component('MyButton', MyButton);
    app.directive('focus', focusDirective);
});

Notesetup() is cumulative — you can call it multiple times and all callbacks will execute in registration order. It applies to every Vue app LiVue creates (root components and islands).

Late Registration — If a package script is loaded after the initial LiVue boot (for example via @livueLoadScript(...) on-request), LiVue applies the new setup() callback to already mounted roots as well.

Execution Order

When LiVue boots a component, this is what happens:

  1. 1
    Vue app created

    createApp() is called.

  2. 2
    Your setup callbacks run

    All LiVue.setup() callbacks execute in order.

  3. 3
    Built-in directives registered

    LiVue's directives (v-click, v-model, etc.) are added.

  4. 4
    App mounted

    The Vue app mounts and the component becomes interactive.

Registration Helpers

Keep registration centralized in LiVue.setup():

Registration methods
LiVue.setup((app) => {
    // Register a Vue plugin
    app.use(MyPlugin, { option: 'value' });

    // Register a global Vue component
    app.component('my-button', MyButton);

    // Register a global directive
    app.directive('focus', focusDirective);
});

Pinia stores from PHP — Use $this->useStore(...) for component-scoped stores and LiVue::createStore(...) in AppServiceProvider for global stores. In component methods or @script, resolve globals with useGlobalStore(name).

Plugin System

LiVue.use(plugin, options) — formal plugin API inspired by Vue's app.use(). Allows bundling hooks, composables, directives, and setup callbacks into a reusable, distributable unit. Plugins are registered before boot and applied during boot(). The three built-in features (progress, devtools, debug) are implemented as plugins and can be disabled.

Plugin interface
const MyPlugin = {
    name: 'my-plugin',  // optional — used for dedup and opt-out
    install(api, options, runtime) {
        // ...
    }
};

LiVue.use(MyPlugin, { /* options */ });

API available inside install(api, options, runtime)

Method Description
api.hook(name, fn) Subscribe to a lifecycle hook. Returns an unsubscribe function.
api.composable(name, value) Expose a global value in all templates as a top-level variable. PHP composables take precedence.
api.directive(name, def) Register a Vue directive on every app instance, applied after built-ins.
api.setup(fn) Add a callback for every Vue app instance (same as LiVue.setup(fn)).
runtime The LiVueRuntime instance, for advanced use cases.

Built-in pluginslivue:progress, livue:devtools, and livue:debug are registered automatically during boot().

Disabling Built-in Plugins

Call removePlugin() before the DOM ready event (before boot() runs):

LiVue.removePlugin('livue:devtools');
LiVue.removePlugin('livue:progress');

Overriding a Built-in Plugin

Register a plugin with the same name as a built-in — deduplication replaces it:

LiVue.use({
    name: 'livue:progress',  // same name → replaces the built-in
    install(api) {
        api.hook('request.started', () => myCustomLoader.show());
        api.hook('request.finished', () => myCustomLoader.hide());
    }
});

Complete Example

resources/js/app.js
import LiVue from 'livue';

// Disable a built-in plugin
LiVue.removePlugin('livue:devtools');

// Register a custom plugin
LiVue.use({
    name: 'my-plugin',
    install(api, options) {
        // Subscribe to lifecycle hooks
        api.hook('component.init', ({ component }) => {
            console.log('init:', component.name);
        });

        // Expose a value in all templates
        api.composable('myService', reactive({ count: 0 }));

        // Register a global directive
        api.directive('highlight', {
            mounted(el, binding) {
                el.style.background = binding.value || 'yellow';
            }
        });

        // Setup callback for every Vue app
        api.setup((app) => {
            app.config.globalProperties.$prefix = options.prefix;
        });
    }
}, { prefix: 'APP' });

// boot() is called automatically at DOM ready

ESM exports — Built-in plugins are exported from the ESM bundle and can be re-used or re-configured: import LiVue, { ProgressPlugin, DevtoolsPlugin, DebugPlugin } from 'livue';

Lifecycle Hooks (JavaScript)

Use LiVue.hook() to listen to component and request lifecycle events. This is useful for analytics, debugging, logging, and integrating third-party libraries.

Lifecycle hooks
// Component initialized
LiVue.hook('component.init', ({ component, el, cleanup }) => {
    console.log('Component initialized:', component.name);

    // Register cleanup for when the component is destroyed
    cleanup(() => {
        console.log('Component cleaned up');
    });
});

// AJAX request started
LiVue.hook('request.started', ({ url, updateCount }) => {
    console.log(`Request to ${url} with ${updateCount} updates`);
});

// AJAX request finished
LiVue.hook('request.finished', ({ url, success, error }) => {
    if (!success) console.error('Request failed:', error);
});

// Template swap
LiVue.hook('template.updated', ({ component, el }) => {
    // Re-initialize a third-party library after DOM update
    initTooltips(el);
});

Available Hooks

Hook Name Payload When
component.init { component, el, cleanup } Component initialized
component.destroy { component, el } Component being destroyed
element.init { el } DOM element initialized
request.started { url, updateCount } AJAX request sent
request.finished { url, success, error } AJAX request completed
template.updating { component, el, html } Before template swap
template.updated { component, el } After template swap
error.occurred { error, componentName, preventDefault } Error on a component

Every hook() call returns an unsubscribe function:

const unsubscribe = LiVue.hook('request.started', callback);

// Later, stop listening
unsubscribe();

Global DOM Events

LiVue dispatches events on document at key moments. These are useful for initializing third-party libraries or running code at specific points in the LiVue lifecycle.

Global events
// Before LiVue initializes — register hooks and plugins here
document.addEventListener('livue:init', () => {
    LiVue.setup((app) => { /* ... */ });
});

// LiVue is fully initialized and components are mounted
document.addEventListener('livue:initialized', () => {
    console.log('LiVue is ready');
});

// After SPA navigation completes
document.addEventListener('livue:navigated', (event) => {
    console.log('Navigated to:', event.detail.url);
    // Re-initialize analytics, tooltips, etc.
});

// Navigation is starting
document.addEventListener('livue:navigating', (event) => {
    console.log('Navigating to:', event.detail.url);
});

Error Handling & Confirmation

Global Error Handler

Register a global error handler to catch all component errors in one place:

LiVue.onError((error, componentName) => {
    console.error('Error on', componentName, ':', error);
    // Send to error tracking service (Sentry, Bugsnag, etc.)
});

// Or use the hook system for more control
LiVue.hook('error.occurred', ({ error, componentName, context, preventDefault }) => {
    sendToErrorTracker({
        message: error.message,
        component: componentName,
        method: context.method,
    });
    preventDefault(); // Suppress default error handling
});

Custom Confirmation Dialog

Replace the default browser confirm() dialog used by the #[Confirm] attribute:

LiVue.setConfirmHandler(async (config) => {
    const result = await Swal.fire({
        title: config.title || 'Confirm',
        text: config.message,
        showCancelButton: true,
        confirmButtonText: config.confirmText,
        cancelButtonText: config.cancelText,
    });
    return result.isConfirmed;
});

Component Access

Access mounted LiVue components from external JavaScript:

// Get the first component on the page
const component = LiVue.first();

// Get all root/island components
const components = LiVue.all();

// Find a component by its ID
const component = LiVue.find('livue-123abc');

// Find components by name (returns array)
const counters = LiVue.getByName('counter');
// Returns: [{ id, name, state, livue }, ...]

Progress Bar

Customize the built-in progress bar shown during AJAX requests and SPA navigation:

LiVue.progress.configure({
    color: '#29d',
    height: '3px',
});

// Manual control
LiVue.progress.start();
LiVue.progress.done();

Custom Attributes

LiVue lets you create custom PHP Attributes with automatic lifecycle behavior. Extend LiVue\Features\SupportAttributes\Attribute, apply the attribute to a property, method, or class, and LiVue handles the rest — zero registration, zero configuration.

Artisan Command

Generate a custom attribute with the Artisan command:

# Property attribute (default)
php artisan make:livue-attribute Trim

# Method attribute
php artisan make:livue-attribute LogCall --target=method

# Class attribute
php artisan make:livue-attribute TrackActivity --target=class

Generated files are placed in app/LiVue/Attributes/ with the appropriate lifecycle methods for the target type.

Property Attribute

Property attributes can transform or validate property values using getValue() and setValue():

app/LiVue/Attributes/Trim.php
namespace App\LiVue\Attributes;

use Attribute;
use LiVue\Features\SupportAttributes\Attribute as LiVueAttribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Trim extends LiVueAttribute
{
    public function hydrate(): void
    {
        $value = $this->getValue();

        if (is_string($value)) {
            $this->setValue(trim($value));
        }
    }
}
Usage in a component
use App\LiVue\Attributes\Trim;

class ContactForm extends Component
{
    #[Trim]
    #[Validate('required|min:3')]
    public string $name = ''; // trimmed BEFORE validation
}

Property with Parameters

Add a constructor to accept parameters:

app/LiVue/Attributes/Clamp.php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Clamp extends LiVueAttribute
{
    public function __construct(
        public readonly int $min = 0,
        public readonly int $max = 100,
    ) {}

    public function hydrate(): void
    {
        $value = $this->getValue();

        if (is_numeric($value)) {
            $this->setValue(max($this->min, min($this->max, $value)));
        }
    }
}
Usage
class Slider extends Component
{
    #[Clamp(min: 0, max: 255)]
    public int $brightness = 128;
}

Method Attribute

Method attributes run when the target method is invoked. They receive the call parameters but do not have access to getValue() / setValue():

app/LiVue/Attributes/LogCall.php
#[Attribute(Attribute::TARGET_METHOD)]
class LogCall extends LiVueAttribute
{
    public function call(array $params): void
    {
        $component = $this->getComponent();

        \Log::info("LiVue: {$component->getName()}::{$this->getName()}", $params);
    }
}
Usage
class AdminPanel extends Component
{
    #[LogCall]
    public function deleteUser(int $id): void { /* ... */ }
}

Class Attribute

Class attributes apply to the entire component. They participate in boot, hydrate, and dehydrate but do not have access to getValue() / setValue():

app/LiVue/Attributes/TrackActivity.php
#[Attribute(Attribute::TARGET_CLASS)]
class TrackActivity extends LiVueAttribute
{
    public function hydrate(): void
    {
        session()->put('last_activity', now());
    }
}
Usage
#[TrackActivity]
class Dashboard extends Component { /* ... */ }

Lifecycle Methods by Target

Available lifecycle methods depend on the attribute target type:

Method Property Method Class
boot()
mount(array $params)
hydrate()
call(array $params)
dehydrate()
getValue() / setValue()

When to use what? — Use Custom Attributes for declarative, opt-in behavior on specific properties, methods, or classes (data transformation, per-method logging, per-component tracking). Use ComponentHook for global, cross-cutting features that must run on every component (global audit logging, exception handling, memo persistence).

Custom PHP Features

On the server side, LiVue's features are implemented as modular ComponentHook classes. Each hook participates in the component lifecycle by overriding only the methods it needs. You can create your own hooks to add custom behavior.

Creating a Hook

Create a class that extends ComponentHook and override the lifecycle methods you need:

app/LiVue/Features/SupportLogging.php
namespace App\LiVue\Features;

use LiVue\Component;
use LiVue\Features\SupportHooks\ComponentHook;
use LiVue\Features\SupportHooks\ComponentStore;

class SupportLogging extends ComponentHook
{
    public function call(
        Component $component,
        ComponentStore $store,
        string $method,
        array $params
    ): void {
        \Log::info("LiVue: {$component->getName()}::{$method}()", $params);
    }

    public function exception(
        Component $component,
        ComponentStore $store,
        \Throwable $e
    ): mixed {
        \Log::error("Error in {$component->getName()}: {$e->getMessage()}");
        return null; // Not handled, let it propagate
    }
}

Registering Hooks

Register your hook in a service provider:

app/Providers/AppServiceProvider.php
use App\LiVue\Features\SupportLogging;
use LiVue\Features\SupportHooks\HookRegistry;

public function boot(): void
{
    $registry = $this->app->make(HookRegistry::class);
    $registry->register(SupportLogging::class);
}

Lifecycle Methods

All lifecycle methods receive the Component and ComponentStore explicitly. Override only the ones you need:

Method When Called
provide() Once at registration time (register routes, views, directives)
boot($component, $store) Every instantiation (mount and update)
mount($component, $store, $params) First render only
hydrate($component, $store) After state restored from snapshot (AJAX)
call($component, $store, $method, $params) Before the requested method executes
dehydrate($component, $store) After execution, before final render
exception($component, $store, $e) On exception (return non-null to handle)
hydrateMemo($component, $store, $memo) Receive data from the incoming snapshot memo
dehydrateMemo($component, $store) Contribute data to the outgoing snapshot memo

Feature Communication

Hooks communicate through the ComponentStore, a per-component key-value store that lives for a single lifecycle cycle:

Feature A sets data, Feature B reads it
// Feature A: Authorization check
public function call(Component $component, ComponentStore $store, ...): void
{
    if ($this->isRestricted($component, $method)) {
        $store->set('auth.blocked', true);
    }
}

// Feature B: Audit logging
public function dehydrate(Component $component, ComponentStore $store): void
{
    if ($store->get('auth.blocked', false)) {
        \Log::warning('Blocked action detected');
    }
}

Persisting Data Between Requests

The ComponentStore is reset on every request. To persist data across requests, use the memo system:

Persisting data via snapshot memo
class SupportRequestCounter extends ComponentHook
{
    // Read from incoming snapshot
    public function hydrateMemo(
        Component $component,
        ComponentStore $store,
        array $memo
    ): void {
        $store->set('requestCount', $memo['requestCount'] ?? 0);
    }

    public function call(...): void
    {
        $store->set('requestCount', $store->get('requestCount') + 1);
    }

    // Write to outgoing snapshot
    public function dehydrateMemo(
        Component $component,
        ComponentStore $store
    ): array {
        return ['requestCount' => $store->get('requestCount')];
    }
}

Asset Management

LiVue provides an asset management system for registering scripts, styles, CSS variables, and script data from packages and service providers. Assets are deduplicated, versioned, and rendered in the correct order.

In a Service Provider
use LiVue\Facades\LiVueAsset;
use LiVue\Features\SupportAssets\Js;
use LiVue\Features\SupportAssets\Css;

public function boot(): void
{
    $this->app->booted(function () {
        // Register scripts and stylesheets
        LiVueAsset::register([
            Css::make('my-styles', url('my-package/styles.css')),
            Js::make('my-script', url('my-package/script.js'))->module(),
        ], 'my-package');

        // CSS variables
        LiVueAsset::registerCssVariables([
            'primary-color' => '#3b82f6',
            'border-radius' => '0.5rem',
        ], 'my-package');

        // Script data (available as window.LiVueData)
        LiVueAsset::registerScriptData([
            'locale' => app()->getLocale(),
            'csrfToken' => csrf_token(),
        ], 'my-package');

        // Import maps for ES modules
        LiVueAsset::registerImports([
            'lodash' => 'https://cdn.jsdelivr.net/npm/lodash-es/lodash.min.js',
        ]);
    });
}

Scoped Assets (On-Request)

For package-specific UI blocks (tables, forms, charts), mark assets with ->onRequest() and load them only in the target Blade view.

LiVueAsset::register([
    Css::make('table-css', url('primix/tables/table.css'))
        ->version('2.4.0')
        ->onRequest(),
    Js::make('table-js', url('primix/tables/table.js'))
        ->version('2.4.0')
        ->onRequest(),
], 'primix/tables');

@livueLoadStyle('table-css', 'primix/tables')
@livueLoadScript('table-js', 'primix/tables')

Dynamic Package Name

Package authors can resolve the package name from the nearest composer.json to avoid hardcoding vendor/name.

$resolvedPackage = LiVueAsset::registerForPackage([
    Css::make('tables-css', url('primix/tables.css')),
    Js::make('tables-js', url('primix/tables.js'))->onRequest(),
], __DIR__);

$packageName = LiVueAsset::resolvePackageName(__DIR__, 'app');

Versioning

Local assets include ?v=... automatically. You can force a specific value with ->version(). The core runtime /livue/livue.js resolves version from package version, then config, then hash fallback.

Asset Options

Js

Js::make('id', '/path.js')
    ->module()       // type="module"
    ->defer(false)   // defer attribute
    ->async()        // async attribute
    ->inline('code') // Inline JS
    ->version('1.2.3') // Force ?v=1.2.3

Css

Css::make('id', '/path.css')
    ->media('print')  // Media query
    ->inline('code')  // Inline CSS
    ->version('1.2.3') // Force ?v=1.2.3

Rendering

Assets are rendered automatically when inject_assets is enabled in config. For manual control, use Blade directives in your layout:

<head>
    @livueStyles    <!-- CSS variables + stylesheets -->
</head>
<body>
    {{ $slot }}

    @livueScripts   <!-- Script data + import maps + JS -->
</body>

Auto-Deduplication

The same asset is never loaded twice, even when registered by multiple components or packages.

Cache Busting

Local assets automatically include a version query string based on your package version.

Package Isolation

Assets are grouped by package name, so each package manages its own scripts and styles independently.