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.
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);
});
Note — setup() 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
Vue app created
createApp()is called. -
2
Your setup callbacks run
All
LiVue.setup()callbacks execute in order. -
3
Built-in directives registered
LiVue's directives (
v-click,v-model, etc.) are added. -
4
App mounted
The Vue app mounts and the component becomes interactive.
Registration Helpers
Keep registration centralized in LiVue.setup():
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.
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 plugins — livue: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
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.
// 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.
// 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():
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));
}
}
}
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:
#[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)));
}
}
}
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():
#[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);
}
}
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():
#[Attribute(Attribute::TARGET_CLASS)]
class TrackActivity extends LiVueAttribute
{
public function hydrate(): void
{
session()->put('last_activity', now());
}
}
#[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:
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:
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: 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:
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.
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.