JavaScript
LiVue's client-side runtime, component API, global utilities, Vue integration and extension system.
Runtime Overview
LiVue includes a lightweight JavaScript runtime that turns server-rendered Blade templates into fully reactive Vue applications. Understanding how this runtime operates helps you write better components and debug issues more effectively.
The entire lifecycle follows a clear sequence:
-
1
Server Renders Blade
Laravel renders your Blade template on the server, producing complete HTML. The component's state and metadata are embedded as
data-livue-snapshotattributes on the wrapper element. -
2
Browser Mounts Vue App
On
DOMContentLoaded, the runtime finds all[data-livue-id]elements, identifies root components, and creates a Vue application for each one. -
3
Dynamic Component Registration
Nested child components are discovered in the HTML and registered as Vue components within the parent's app. Each child gets its own reactive state and
livuehelper instance. -
4
State Refs Created from Snapshot
The JSON snapshot is parsed and wrapped in Vue's
reactive(). A plain copy is kept as the server-confirmed baseline for computing diffs on the next request. -
5
AJAX Layer Handles Communication
When a user triggers an action, the runtime computes the state diff, batches it with other pending calls from the same tick, and sends a single HTTP request. Multiple server calls in the same handler are automatically pooled into a single HTTP request.
-
6
Template Swap on Response
The server responds with the updated snapshot and re-rendered HTML. The runtime updates the reactive state, swaps the component definition via a
shallowRef, and Vue re-renders the DOM. No DOM morphing — a clean template replacement.
Single App by Default — All components on a page share one Vue application. Root components and their children live in the same Vue instance. If you need isolation, mark a component as an Island to give it its own independent Vue app.
Component API (livue.*)
Every component template has access to a livue helper object. This object is available in your Blade templates and inside @script blocks. It provides methods for calling the server, managing state, watching changes, handling errors, and more.
Methods
| Method | Description |
|---|---|
livue.toggle(prop) |
Toggle a boolean property on the client. |
livue.set(prop, value) |
Set a property value on the client without a server call. |
livue.store(name, definition, options?) |
Create a quick Pinia store, or reuse an existing one by calling it without definition. |
livue.useStore(name) |
Resolve a pre-registered store (component scope first, then global). |
livue.useGlobalStore(name) |
Resolve a pre-registered global store only. |
livue.sync() |
Sync the current client state to the server without calling a method. |
livue.watch(prop, callback) |
Watch a property for changes. Returns an unwatch function. |
livue.upload(prop, file) |
Upload a file to the given property. |
livue.dispatch(event, data) |
Dispatch an event to all listeners. |
livue.dispatchTo(target, event, data) |
Dispatch an event to a specific component. |
livue.navigate(url) |
Trigger SPA navigation to a URL. |
livue.clearErrors() |
Clear all validation errors. |
Properties
| Property | Type | Description |
|---|---|---|
livue.loading |
boolean | True while any AJAX request is in flight. |
livue.processing |
string|null | Name of the method currently being processed. |
livue.errors |
object | Programmatic validation errors object (arrays per field). Template shorthand: $errors (returns first message string directly). |
livue.$el |
HTMLElement | The component's root DOM element. |
livue.$id |
string | The component's unique identifier. |
livue.$name |
string | The component name in kebab-case. |
livue.$parent |
object|null | Parent component's livue helper, or null for root. |
Calling Server Methods
Server methods are available directly in the template — just call them like regular functions.
<!-- Direct call (recommended) -->
<button @click="save()">Save</button>
<!-- With arguments -->
<button @click="delete(item.id)">Delete</button>
<!-- Multiple arguments -->
<button @click="update(item.id, { name: 'New Name' })">Update</button>
<!-- Declarative with v-click -->
<button v-click="save">Save</button>
Toggling Boolean Properties
livue.toggle() flips a boolean property on the client side, without making a server round-trip.
<button @click="livue.toggle('showDetails')">
{{ showDetails ? 'Hide' : 'Show' }} Details
</button>
<div v-if="showDetails">
<p>Detailed content here...</p>
</div>
Watching State Changes
livue.watch() registers a callback that fires whenever a specific property changes. It returns an unwatch function you can call to stop listening.
// Watch a property for changes
const unwatch = livue.watch('count', (newValue, oldValue) => {
console.log('Count changed from', oldValue, 'to', newValue);
});
// Stop watching when no longer needed
unwatch();
Quick Pinia Stores
If a store is declared in PHP, you can resolve it directly in @script with useStore(name) and sync frontend edits with livue.sync().
const componentStore = useStore('demo-counter');
const globalStore = livue.useGlobalStore('demo-global-counter');
// Frontend updates (local until sync)
componentStore.count++;
globalStore.count++;
const syncToBackend = () => livue.sync();
Stores declared from PHP expose state (and optional bridge rules). For frontend actions, mutate store state directly or call a PHP method, then use livue.sync() when you want to persist client changes.
Accessing the Root Element
livue.$el gives you a reference to the component's root DOM element. This is useful for interacting with third-party libraries or measuring layout.
// Access the root DOM element
const width = livue.$el.offsetWidth;
console.log('Component width:', width);
// Scroll into view
livue.$el.scrollIntoView({ behavior: 'smooth' });
Validation Errors
The $errors magic variable is the recommended way to display validation errors in templates. Each key returns the first error message string directly. The programmatic livue.errors object is also available (returns arrays per field).
<div>
<input v-model-livue="email" type="email">
<!-- Show error for a specific field -->
<p v-if="$errors.email" class="text-red-500">
{{ $errors.email }}
</p>
<!-- Loop through all errors (programmatic API) -->
<ul v-if="Object.keys(livue.errors).length">
<li v-for="(messages, field) in livue.errors">
{{ field }}: {{ messages[0] }}
</li>
</ul>
<button @click="livue.clearErrors()">Clear Errors</button>
</div>
File Uploads
Use livue.upload() to send files to the server. You can track progress through the reactive livue.uploading and livue.uploadProgress properties.
<input
type="file"
@change="livue.upload('avatar', $event.target.files[0])"
/>
<div v-if="livue.uploading">
Uploading: {{ livue.uploadProgress }}%
</div>
Global API (LiVue.*)
The global LiVue object is available on window.LiVue. It provides methods for finding components, navigating, registering hooks, and handling errors at the application level.
Methods
| Method | Description |
|---|---|
LiVue.find(id) |
Find a component instance by its unique ID. |
LiVue.first() |
Get the first component on the page. |
LiVue.all() |
Get all root and island component instances. |
LiVue.getByName(name) |
Find all components matching a given name. |
LiVue.hook(name, callback) |
Register a lifecycle hook callback. Returns an unsubscribe function. |
LiVue.onError(callback) |
Register a global error handler. |
LiVue.navigate(url) |
Trigger SPA navigation to a URL programmatically. |
LiVue.setup(callback) |
Register a Vue app configuration callback. |
Finding Components
// Get the first component on the page
const component = LiVue.first();
// Get all root/island components
const components = LiVue.all();
console.log('Found', components.length, 'components');
// Find a specific component by ID
const cart = LiVue.find('livue-abc123');
console.log(cart.state);
// Find all components by name
const counters = LiVue.getByName('counter');
// Returns: [{ id, name, state, livue }, ...]
Global DOM Events
LiVue dispatches native DOM events on the document at key points in its lifecycle. These are useful for integrations that need to run before or after LiVue initializes, or after SPA navigations.
// Fired after LiVue loaded but before initialization
document.addEventListener('livue:init', () => {
// Register hooks, plugins, etc.
});
// Fired after LiVue fully initialized
document.addEventListener('livue:initialized', () => {
// LiVue is ready, all components mounted
});
// Fired after SPA navigation completes
document.addEventListener('livue:navigated', (event) => {
console.log('Navigated to:', event.detail.url);
});
Vue Registration
LiVue.setup() is the entry point for configuring the Vue applications created by LiVue. It accepts a callback that receives the Vue app instance, allowing you to register plugins, components, directives, and anything else you would normally do with app.use() or app.component().
The callback runs for every Vue app LiVue creates — both root components and islands. You can call setup() multiple times; all callbacks are executed in registration order.
Basic Setup
Import LiVue from the ESM bundle and call setup() in your app.js. When setup() is called, LiVue automatically switches to ESM mode and shares your Vue instance instead of loading its standalone bundle.
import LiVue from 'livue';
LiVue.setup((app) => {
// app is a Vue app instance (from createApp())
// Configure it however you need
});
Registering Global Components
import LiVue from 'livue';
import MyButton from './components/MyButton.vue';
import DataTable from './components/DataTable.vue';
LiVue.setup((app) => {
app.component('MyButton', MyButton);
app.component('DataTable', DataTable);
});
Once registered, you can use these components directly in your Blade templates like any other Vue component: <MyButton label="Click me" />.
Registering Global Directives
import LiVue from 'livue';
LiVue.setup((app) => {
app.directive('focus', {
mounted(el) {
el.focus();
}
});
app.directive('tooltip', {
mounted(el, binding) {
el.title = binding.value;
},
updated(el, binding) {
el.title = binding.value;
}
});
});
Vite Alias
Since LiVue is distributed via Composer (not npm), you need a Vite alias to import it cleanly:
import { defineConfig } from 'vite';
import { resolve } from 'path';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
resolve: {
alias: {
'livue': resolve(__dirname, 'vendor/livue/livue/dist/livue.esm.js'),
},
},
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
Execution Order — When a Vue app is created, Pinia is installed first (required internally), then all setup() callbacks run in registration order, then LiVue's built-in directives are registered, and finally the app is mounted.
Extending LiVue
LiVue integrates naturally with the Vue ecosystem. Any Vue plugin, UI framework, or state management library that works with app.use() can be plugged in through LiVue.setup().
Vuetify
Install Vuetify with its theme and styles, then register it through the setup callback:
import LiVue from 'livue';
import { createVuetify } from 'vuetify';
import 'vuetify/styles';
const vuetify = createVuetify({
theme: {
defaultTheme: 'dark'
}
});
LiVue.setup((app) => {
app.use(vuetify);
});
Pinia Stores
Pinia is already installed on every Vue app by LiVue (it uses Pinia internally). Define component stores in the component class and global stores at app bootstrap:
protected function defineStores(): array
{
return [
'demo-counter' => [
'state' => ['count' => $this->serverCount],
'bridge' => ['count' => 'serverCount'],
],
];
}
use LiVue\Facades\LiVue;
public function boot(): void
{
// Global stores are defined once at bootstrap
LiVue::createStore('demo-global-counter', [
'state' => ['count' => 100],
'bridge' => ['count' => 'globalCount'],
]);
LiVue::createStore('demo-app-counter', [
'state' => ['count' => 500],
]);
}
const componentStore = useStore('demo-counter');
const globalStore = livue.useGlobalStore('demo-global-counter');
const appGlobalStore = livue.useGlobalStore('demo-app-counter');
componentStore.count++;
globalStore.count++;
appGlobalStore.count++;
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
});
Custom Vue Plugins
Register any Vue plugin through setup(). Multiple calls are cumulative, so frameworks built on LiVue can register their own plugins while still allowing users to add more:
import LiVue from 'livue';
import { createVuetify } from 'vuetify';
import { createI18n } from 'vue-i18n';
const vuetify = createVuetify({ /* ... */ });
const i18n = createI18n({ /* ... */ });
// Register multiple plugins in one call
LiVue.setup((app) => {
app.use(vuetify);
app.use(i18n);
});
// Or spread across multiple setup() calls
LiVue.setup((app) => {
app.use(vuetify);
});
LiVue.setup((app) => {
app.use(i18n);
});
Plugin API
Configure plugins, components and directives inside LiVue.setup():
LiVue.setup((app) => {
app.use(MyPlugin, { option: 'value' });
app.component('my-button', MyButton);
app.directive('focus', focusDirective);
});
Note — In PHP, useStore() (or defineStores()) declares component stores, while LiVue::createStore() declares globals. Use useGlobalStore(name) only to resolve a global store.
Error Boundaries
LiVue provides built-in error handling for AJAX failures. By default, errors are logged to the browser console. You can customize this behavior at the global level or per component.
Default Behavior
When an AJAX request fails (network error, server exception, validation error), LiVue logs the error to the console and resets the component's loading state. The UI remains functional and the user can retry the action.
Global Error Handler
Register a custom error handler with LiVue.onError() to intercept all errors across your application. This is the ideal place to integrate toast notifications, error tracking services, or custom retry logic.
import LiVue from 'livue';
// Simple error handler
LiVue.onError((error, componentName) => {
console.error('Error on', componentName, ':', error);
});
Toast Notification Integration
A practical example showing how to display user-friendly toast messages when errors occur:
import LiVue from 'livue';
import { toast } from 'vue-sonner';
LiVue.onError((error, componentName) => {
// Show toast to the user
toast.error('Something went wrong. Please try again.');
// Send to error tracking service
Sentry.captureException(error, {
tags: { component: componentName }
});
});
Component Error State
Components with the #[ErrorBoundary] PHP attribute expose a reactive livue.errorState object in the template. You can use it to display inline error messages and recovery options:
use LiVue\Attributes\ErrorBoundary;
#[ErrorBoundary(
recover: true,
showDetails: true,
message: 'Oops! Something went wrong.'
)]
class Dashboard extends Component
{
// ...
}
<div v-if="livue.errorState.hasError"
class="bg-red-900/20 border border-red-800 p-4 rounded">
<p>{{ livue.errorState.errorMessage }}</p>
<p v-if="livue.errorState.errorDetails"
class="text-sm text-gray-400 mt-2">
{{ livue.errorState.errorDetails }}
</p>
<button
@click="livue.clearError()"
v-if="livue.errorState.recover">
Dismiss
</button>
</div>
Error Hook
For more granular control, use the error.occurred lifecycle hook. It receives error details and a preventDefault() function to suppress default handling:
LiVue.hook('error.occurred', ({ error, componentName, componentId, context, preventDefault }) => {
// Prevent default error handling
preventDefault();
// Custom error reporting
sendToErrorTracker({
message: error.message,
component: componentName,
method: context.method,
});
});
Client-side Hooks
LiVue.hook() lets you intercept lifecycle events across the entire application. Hooks are ideal for logging, analytics, custom loading indicators, and other cross-cutting concerns. Each call returns an unsubscribe function.
Available Hooks
| Hook | Description |
|---|---|
component.init |
Fired when a component is initialized (root or child). Provides component, el, and a cleanup callback. |
component.destroy |
Fired when a component is being destroyed. |
element.init |
Fired when a component's DOM element is initialized. |
request.started |
Fired when an AJAX request is sent. Provides url and updateCount. |
request.finished |
Fired when an AJAX request completes. Provides url, success, and error. |
template.updating |
Fired before a template is swapped. Provides component, el, and html. |
template.updated |
Fired after a template swap completes. Provides component and el. |
error.occurred |
Fired when an error occurs. Provides error, componentName, and preventDefault. |
Component Lifecycle Tracking
const unsubscribe = LiVue.hook('component.init', ({ component, el, cleanup }) => {
console.log('Component initialized:', component.name);
// Register a cleanup function that runs when the component is destroyed
cleanup(() => {
console.log('Component destroyed:', component.name);
});
});
// Later, stop listening to all component.init events
unsubscribe();
Request Tracking
Use request hooks to build custom loading indicators, log performance metrics, or integrate with analytics:
let requestStart;
LiVue.hook('request.started', ({ url, updateCount }) => {
requestStart = performance.now();
console.log(`Request to ${url} with ${updateCount} updates`);
});
LiVue.hook('request.finished', ({ url, success, error }) => {
const duration = performance.now() - requestStart;
if (success) {
console.log(`Request completed in ${duration.toFixed(0)}ms`);
} else {
console.error('Request failed:', error);
}
});
Analytics Integration
A practical example showing how to build a custom analytics plugin using hooks:
export function initAnalytics() {
// Track component usage
LiVue.hook('component.init', ({ component }) => {
analytics.track('component_mounted', {
name: component.name,
});
});
// Track server interactions
LiVue.hook('request.started', ({ url, updateCount }) => {
analytics.track('server_request', {
url,
updates: updateCount,
});
});
// Track errors
LiVue.hook('error.occurred', ({ error, componentName }) => {
analytics.track('component_error', {
component: componentName,
message: error.message,
});
});
}
Custom Loading Indicator
Build a global loading bar that tracks all pending requests:
let activeRequests = 0;
const loader = document.getElementById('global-loader');
LiVue.hook('request.started', () => {
activeRequests++;
loader.classList.remove('hidden');
});
LiVue.hook('request.finished', () => {
activeRequests--;
if (activeRequests === 0) {
loader.classList.add('hidden');
}
});