Features
Built-in features that handle common tasks: file uploads and downloads, real-time streaming, SPA navigation, confirm dialogs, loading indicators, tab synchronization, benchmarking, pagination and reusable PHP composables.
File Uploads
LiVue provides a complete file upload system with temporary storage, progress tracking, image previews and validation. Files are uploaded via a dedicated endpoint, stored temporarily on the server, and then your component decides where to persist them.
Enabling Uploads
Add the WithFileUploads trait to your component. Properties are detected automatically by their type hint:
<?php
namespace App\LiVue;
use LiVue\Component;
use LiVue\Features\SupportFileUploads\WithFileUploads;
use LiVue\Features\SupportFileUploads\TemporaryUploadedFile;
class PhotoUploader extends Component
{
use WithFileUploads;
public ?TemporaryUploadedFile $photo = null;
public array $documents = [];
public function fileRules(): array
{
return [
'photo' => ['image', 'max:2048'], // 2MB, images only
'documents' => ['file', 'max:5120'], // 5MB, any file
];
}
public function savePhoto(): void
{
// Store with auto-generated name
$path = $this->photo->store('photos');
// Or store with a specific name
// $path = $this->photo->storeAs('photos', 'avatar.jpg');
$this->photo = null;
}
}
Template
Use livue.upload() to handle the file input. After upload, the property contains metadata for display including a temporary preview URL for images:
<div>
<!-- File input -->
<input type="file" accept="image/*"
@change="livue.upload('photo', $event.target.files[0])">
<!-- Progress bar -->
<div v-if="livue.uploading">
Uploading... {{ livue.uploadProgress }}%
<div :style="{ width: livue.uploadProgress + '%' }"></div>
</div>
<!-- Preview after upload -->
<div v-if="photo">
<img v-if="photo.previewUrl" :src="photo.previewUrl">
<span v-text="photo.originalName"></span>
<button @click="livue.removeUpload('photo')">Remove</button>
</div>
<!-- Errors -->
<span v-if="$errors.photo" v-text="$errors.photo"></span>
<button @click="savePhoto()">Save</button>
</div>
Multiple File Uploads
For array properties, use livue.uploadMultiple(). All files are sent in a single HTTP request. Valid files are accepted even if others fail validation:
<input type="file" multiple
@change="livue.uploadMultiple('documents', $event.target.files)">
<div v-for="(doc, index) in documents" :key="index">
{{ doc.originalName }} - {{ Math.round(doc.size / 1024) }} KB
<button @click="livue.removeUpload('documents', index)">Remove</button>
</div>
TemporaryUploadedFile Methods
After a successful upload, the PHP property holds a TemporaryUploadedFile with these methods:
| Method | Description |
|---|---|
store($dir, $disk) |
Store with a random filename. Returns the path. |
storeAs($dir, $name, $disk) |
Store with a specific filename. Returns the path. |
temporaryUrl() |
Returns a signed URL for image preview (expires in 30 min). |
getOriginalName() |
Returns the original file name. |
getMimeType() |
Returns the MIME type. |
getSize() |
Returns the file size in bytes. |
delete() |
Deletes the temporary file. |
Tip — Validation rules defined in fileRules() are applied at upload time, before the file is accepted. This means invalid files are rejected immediately, not when you call validate().
File Downloads
Trigger file downloads from server-side actions. LiVue provides two methods: one for existing files on disk and another for dynamically generated content.
download()
Download a file from the filesystem or a storage disk:
public function downloadReport(): void
{
// From local filesystem
$this->download(storage_path('reports/monthly.pdf'), 'report.pdf');
}
public function downloadInvoice(int $id): void
{
$invoice = Invoice::findOrFail($id);
// From a storage disk (e.g. S3)
$this->download(
path: $invoice->file_path,
name: "invoice-{$invoice->number}.pdf",
disk: 's3'
);
}
downloadContent()
Generate content on the fly and trigger a download. This is useful for exporting data as CSV, JSON or any other format:
public function exportCsv(): void
{
$users = User::all();
$csv = "Name,Email\n";
foreach ($users as $user) {
$csv .= "{$user->name},{$user->email}\n";
}
$this->downloadContent($csv, 'users.csv', [
'Content-Type' => 'text/csv',
]);
}
Template
Downloads are triggered from the template like any other action. The file is downloaded in the background without blocking the UI:
<button @click="exportCsv()" :disabled="livue.loading">
<span v-if="livue.isLoading('exportCsv')">Generating...</span>
<span v-else>Export CSV</span>
</button>
Tip — Under the hood, the server creates an encrypted token containing the file path or content. The JavaScript runtime then fetches the file via a separate GET /livue/download endpoint. The token expires after 5 minutes.
Streaming
Stream content from the server to the client in real-time during long-running operations. This uses HTTP streaming (not WebSockets) via NDJSON, making it ideal for AI text generation, live logs, progress updates and more.
Enabling Streaming
Add the WithStreaming trait and use the stream() method to send data chunks to the client:
<?php
namespace App\LiVue;
use LiVue\Component;
use LiVue\Features\SupportStreaming\WithStreaming;
class AiChat extends Component
{
use WithStreaming;
public string $response = '';
public function ask(string $question): void
{
$stream = $this->openai->chat($question, stream: true);
foreach ($stream as $chunk) {
$this->stream(to: 'output', content: $chunk);
}
$this->response = $stream->getFullResponse();
}
}
The stream() Method
Each call sends a chunk to a named target in the template. By default, content is appended. Use replace: true to overwrite the previous content instead:
// Append mode (default) - content accumulates
$this->stream(to: 'output', content: 'Hello ');
$this->stream(to: 'output', content: 'World!');
// Result: "Hello World!"
// Replace mode - content is overwritten
$this->stream(to: 'progress', content: '50%', replace: true);
$this->stream(to: 'progress', content: '100%', replace: true);
// Result: "100%"
v-stream Directive
Mark elements as stream targets with the v-stream directive. Use livue.stream() instead of a regular method call to invoke the streaming method:
<!-- Append mode (default) -->
<div v-stream="'output'">Waiting for response...</div>
<!-- Replace mode -->
<div v-stream.replace="'status'">Ready</div>
<!-- Blinking cursor while streaming -->
<span v-if="livue.streaming" class="animate-pulse">|</span>
<button
@click="livue.stream('ask', ['What is Vue?'])"
:disabled="livue.streaming"
>
<span v-if="livue.streaming">Generating...</span>
<span v-else>Ask AI</span>
</button>
Tip — Streaming requests are automatically isolated from the request pooling system. The component state is updated normally when the stream finishes. You can also stream to multiple targets simultaneously.
SPA Navigation
LiVue supports client-side navigation that fetches pages via AJAX and swaps the body content without a full page reload. This provides an SPA-like experience with prefetching, scroll restoration and persistent components.
v-navigate Directive
Add v-navigate to any link to enable SPA navigation. Links are prefetched automatically:
<!-- Prefetch on mousedown (default) -->
<a href="/dashboard" v-navigate>Dashboard</a>
<!-- Prefetch on hover (more aggressive, after 60ms delay) -->
<a href="/posts" v-navigate.hover>Posts</a>
<!-- Programmatic navigation -->
<button @click="livue.navigate('/settings')">Settings</button>
Server-side Redirect
Components can trigger navigation from the server. Use the navigate parameter to choose between a full page reload and an SPA transition:
public function save(): void
{
$this->validate();
$this->model->save();
// SPA navigation (no full reload)
$this->redirect('/items', navigate: true);
}
public function cancel(): void
{
// Classic redirect (full page reload)
$this->redirect('/');
}
@persist Directive
Preserve DOM elements and their state across SPA page transitions. This is useful for video players, chat widgets, sidebars and LiVue components that should maintain their internal state:
<!-- Video keeps playing across pages -->
@persist('player')
<video autoplay controls>
<source src="...">
</video>
@endpersist
<!-- LiVue component preserves its Vue state -->
@persist('sidebar')
@livue('docs-sidebar')
@endpersist
Prefetching Strategies
| Strategy | When | Use case |
|---|---|---|
v-navigate |
Between mousedown and click (~100ms) | Default, balanced |
v-navigate.hover |
After 60ms of hover | Faster navigation, more requests |
Tip — Pages visited during SPA navigation are cached in memory for instant back/forward. Scroll positions are saved and restored automatically. External links and modifier-key clicks (Cmd/Ctrl) bypass SPA navigation.
Confirm Dialogs
Add confirmation prompts before executing destructive or important actions. By default, LiVue uses the browser's native confirm() dialog, but you can plug in any custom modal library.
#[Confirm] Attribute
Add the #[Confirm] attribute to any method. When the method is called from the template, the user is prompted before the AJAX request is sent:
use LiVue\Attributes\Confirm;
class TaskManager extends Component
{
// Simple message
#[Confirm('Are you sure you want to delete this task?')]
public function delete(int $id): void
{
Task::destroy($id);
}
// With title and custom button text
#[Confirm(
message: 'This action cannot be undone.',
title: 'Delete All Tasks',
confirmText: 'Yes, delete all',
cancelText: 'No, keep them'
)]
public function deleteAll(): void
{
Task::truncate();
}
}
Custom Confirm Handler
Replace the default browser dialog with your own UI library such as SweetAlert2, a Vue modal or any promise-based confirmation:
import LiVue from 'livue';
import Swal from 'sweetalert2';
LiVue.setConfirmHandler(async (config) => {
// config = { message, title, confirmText, cancelText }
const result = await Swal.fire({
title: config.title || 'Confirm',
text: config.message,
icon: 'warning',
showCancelButton: true,
confirmButtonText: config.confirmText || 'Confirm',
cancelButtonText: config.cancelText || 'Cancel',
});
return result.isConfirmed;
});
LiVue.start();
Tip — The confirm check runs entirely on the client side. If the user cancels, no AJAX request is sent. Loading indicators only appear after the user confirms.
Loading States
Track whether actions are in progress and provide visual feedback. LiVue offers both a JavaScript API and dedicated directives for managing loading states.
JavaScript API
The livue helper provides reactive properties and methods for loading state:
| API | Description |
|---|---|
livue.loading |
true when any action is in progress |
livue.isLoading() |
Returns true during any AJAX request |
livue.isLoading('save') |
Returns true only while the save method is running |
livue.processing |
Name of the method currently executing |
livue.loadingAttrs('save') |
Returns { disabled, 'aria-busy' } bindings |
<!-- Per-action loading -->
<button @click="save()" :disabled="livue.isLoading('save')">
<span v-if="livue.isLoading('save')">Saving...</span>
<span v-else>Save</span>
</button>
<!-- Using loadingAttrs for accessible bindings -->
<button @click="submit()" v-bind="livue.loadingAttrs('submit')">
Submit
</button>
v-loading Directive
The v-loading directive provides a declarative way to show, hide or style elements based on loading state:
<!-- Show only when loading -->
<div v-loading>Loading...</div>
<!-- Show only when specific action loads -->
<div v-loading="'save'">Saving...</div>
<!-- Hide content during loading -->
<div v-loading.remove>Content hidden while loading</div>
<!-- Add CSS class during loading -->
<div v-loading.class="'opacity-50 pointer-events-none'">Fades when loading</div>
<!-- Add disabled attribute -->
<button v-loading.attr="'disabled'">Submit</button>
<!-- Delay to prevent flash on fast requests -->
<div v-loading.delay.short>Shows after 150ms</div>
v-target Directive
The v-target directive adds a data-loading attribute during loading, perfect for Tailwind's data-[loading]: variant:
<button
@click="delete()"
v-target="'delete'"
class="bg-red-500 text-white
data-[loading]:opacity-50
data-[loading]:cursor-wait"
>
Delete
</button>
Progress Bar
LiVue includes a built-in progress bar (similar to NProgress) that shows during SPA navigation. By default, the progress bar is hidden during AJAX requests (component updates). You can enable it via configuration if desired.
Enabling on AJAX Requests
To show the progress bar during component update requests, enable it in your config:
'progress' => [
'show_on_request' => true,
],
Appearance Configuration
Customize the appearance and behavior via the JavaScript API:
LiVue.progress.configure({
color: '#3b82f6', // Bar color (default: '#29d')
height: '3px', // Bar height (default: '2px')
speed: 300, // Animation speed in ms (default: 200)
trickle: true, // Enable trickle animation (default: true)
trickleSpeed: 300, // Trickle speed in ms (default: 200)
});
Manual Control
For custom async operations outside of LiVue, you can control the progress bar programmatically:
// Start the progress bar
LiVue.progress.start();
// Set a specific value (0 to 1)
LiVue.progress.set(0.5);
// Complete the bar
LiVue.progress.done();
// Check if active
LiVue.progress.isStarted();
Disabling on Navigation
The progress bar is already hidden by default on AJAX requests. To also disable it during SPA navigation:
'navigate' => [
'show_progress_bar' => false,
],
Tip — When enabled, the progress bar integrates with the request pooling system. It stays active across concurrent requests and only completes when all pending requests have resolved.
Tab Sync
Synchronize component state across browser tabs automatically. When a component is modified in one tab, all other tabs with the same component receive the update in real-time using the BroadcastChannel API.
#[TabSync] Attribute
Add the #[TabSync] attribute to your component class. Changes made via server actions in one tab will automatically appear in all other open tabs:
use LiVue\Attributes\TabSync;
#[TabSync]
class ShoppingCart extends Component
{
public array $items = [];
public float $total = 0;
public function addItem(array $item): void
{
$this->items[] = $item;
$this->calculateTotal();
}
}
Selective Sync
Control which properties are synchronized using only or except:
// Only sync specific properties
#[TabSync(only: ['items', 'total'])]
class ShoppingCart extends Component
{
public array $items = []; // Synced
public float $total = 0; // Synced
public string $coupon = ''; // NOT synced
}
// Exclude specific properties
#[TabSync(except: ['searchQuery'])]
class ProductList extends Component
{
public array $products = []; // Synced
public string $searchQuery = ''; // NOT synced
}
Reactive Mode
By default, synced properties update only the Vue state. If your template uses Blade-rendered values ({{ $count }}), enable reactive mode to trigger a server re-render:
// All properties trigger server refresh
#[TabSync(reactive: true)]
class Counter extends Component { ... }
// Only specific properties trigger server refresh
#[TabSync(reactive: ['count', 'items'])]
class Dashboard extends Component
{
public int $count = 0; // Triggers server refresh
public string $filter = ''; // Vue-only update (no refresh)
}
Tip — Tab sync uses the BroadcastChannel API with a localStorage fallback for older browsers. Only changes from server actions are broadcast; local v-model changes are not synced unless they accompany a server action. This is ideal for shopping carts, notification counts and theme preferences.
Benchmarking
LiVue provides tools to measure the performance of every phase in the component lifecycle. You can profile components from the CLI with detailed per-phase breakdowns, or enable inline response timing to inspect server-side performance directly in the browser DevTools.
CLI Command
The livue:benchmark Artisan command profiles a component's mount and update lifecycle across multiple iterations and reports min, max, average and median timing for each phase, plus peak memory usage.
# Benchmark mount lifecycle (10 iterations by default)
php artisan livue:benchmark Counter
# Benchmark a specific method call (update lifecycle)
php artisan livue:benchmark Counter --method=increment
# More iterations for stable results
php artisan livue:benchmark Counter --iterations=50
# JSON output for CI or tooling
php artisan livue:benchmark Counter --format=json
# Pass mount parameters
php artisan livue:benchmark Counter --mount-params='{"start":10}'
# Pass method parameters
php artisan livue:benchmark Counter --method=increment --params='[5]'
# Pass context variables (JSON object)
php artisan livue:benchmark Counter --context='{"tenant_id": 1, "locale": "en"}'
# Use a custom setup class for environment configuration
php artisan livue:benchmark ListProducts --setup="App\Support\TenantSetup" --context='{"tenant_id": 1}'
Options
| Option | Description |
|---|---|
component |
Component name or fully-qualified class name (required) |
--iterations |
Number of iterations to run (default: 10) |
--method |
Method to call during update lifecycle benchmarking |
--params |
JSON array of parameters for the method |
--mount-params |
JSON object of parameters for mount |
--format |
Output format: table (default) or json |
--context |
JSON object with context variables for environment setup |
--setup |
Fully qualified class implementing BenchmarkSetup for custom setUp/tearDown |
Measured Phases
The command measures every internal lifecycle phase. During mount: boot, mount, forms, render, snapshot and total. During update: boot, hydrate_state, baseline_render, apply_diffs, distribute_memo, hydrate_hooks, method_call, dehydrate, events_flush, build_snapshot, render, collect_memo and total.
Results are color-coded with differentiated thresholds. Framework phases (boot, hydrate, dehydrate, etc.) use stricter limits: green <5ms, yellow 5-20ms, red >20ms. User phases (mount, method_call, render, total) use relaxed limits: green <50ms, yellow 50-200ms, red >200ms.
Environment Setup
The benchmark command instantiates components with new $class() outside any HTTP context — no request, session, or middleware. Components that depend on application state (e.g. a tenant context, authenticated user, or service singletons initialized by middleware) will fail without explicit setup.
LiVue provides two complementary mechanisms to configure the environment before benchmarking.
BenchmarkSetup Interface
Create a class implementing LiVue\Console\Contracts\BenchmarkSetup to run setup logic before the benchmark and cleanup after.
use LiVue\Console\Contracts\BenchmarkSetup;
class TenantSetup implements BenchmarkSetup
{
public function setUp(array $context): void
{
// Initialize whatever your components need
if (isset($context['tenant_id'])) {
Tenancy::initialize($context['tenant_id']);
}
}
public function tearDown(): void
{
Tenancy::end();
}
}
php artisan livue:benchmark ListProducts --setup="App\Support\TenantSetup" --context='{"tenant_id": 1}'
The setUp() method receives the parsed --context array. The tearDown() method runs in a finally block, so it executes even if the benchmark throws an exception.
Automatic Setup via Events
For packages that need to configure the environment automatically (without requiring the user to pass --setup), LiVue dispatches two Laravel events around the benchmark:
| Event | When | Properties |
|---|---|---|
BenchmarkStarting |
Before iterations begin | componentClass, context, method, iterations |
BenchmarkFinished |
After iterations complete | componentClass, context, mountResults, updateResults |
use LiVue\Events\BenchmarkStarting;
use LiVue\Events\BenchmarkFinished;
// In your ServiceProvider boot()
Event::listen(BenchmarkStarting::class, function (BenchmarkStarting $event) {
if (isset($event->context['tenant_id'])) {
Tenancy::initialize($event->context['tenant_id']);
}
});
Event::listen(BenchmarkFinished::class, function () {
Tenancy::end();
});
Tip — Both mechanisms work together. Use --setup for one-off setup needs and event listeners for automatic setup that activates whenever a package is installed.
Inline Response Timing
Enable per-phase timing data in every AJAX response. This adds a benchmark key to the JSON response with timing for each lifecycle phase in microseconds.
'benchmark_responses' => env('LIVUE_BENCHMARK', false),
LIVUE_BENCHMARK=true
When enabled, the benchmark data is visible in the DevTools Performance tab under the "Server Lifecycle Timing" section, giving you a per-phase breakdown of every server round-trip.
Tip — Benchmarking is only available in local/development environments and is off by default. There is no performance overhead in production. Enable it via LIVUE_BENCHMARK=true in your .env file when you need it.
PHP Composables
PHP Composables are reusable units of server-side logic that expose data and actions directly to your Vue templates under clean namespaces. Create a trait with a use* method, add it to your component, and access everything via namespace.property or namespace.action() in the template.
class Dashboard extends Component
{
use UseAuth, UseCart;
}
// Template: auth.name, auth.logout(), cart.items, cart.add(42)
Composables support auto-discovery, private persistent state, global registration from service providers, and more.
Read the full Composables guide →