Components
LiVue components are PHP classes that define reactive state and actions,
paired with Blade templates that use Vue directives for interactivity.
Every component extends the abstract Component base class and implements a
render() method that returns a Blade view name.
Basic Structure
A LiVue component consists of two parts: a PHP class that extends
LiVue\Component and a Blade template. The class defines reactive state through
public properties and callable actions through public methods. The
render() method tells LiVue which Blade view to use as the template.
<?php
namespace App\LiVue;
use LiVue\Component;
class Counter extends Component
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
protected function render(): string
{
return 'livue.counter';
}
}
<div>
<p>Count: {{ count }}</p>
<button v-click="increment">
Add
</button>
</div>
Public properties become reactive Vue state, automatically synchronized between the PHP server and the Vue client.
Public methods become callable actions that can be triggered from templates using directives like
v-click.
Tip — Generate a new component with php artisan make:livue Counter. This creates both the PHP class and the Blade template.
Properties
Every public property on a LiVue component becomes reactive Vue state. Properties are serialized into a JSON snapshot sent to the client. When the client updates a property (e.g., via v-model), the new value is sent back to the server on the next AJAX request.
class UserProfile extends Component
{
// String
public string $name = '';
// Integer
public int $age = 0;
// Float
public float $rating = 0.0;
// Boolean
public bool $active = true;
// Array
public array $items = [];
// Nullable
public ?string $email = null;
}
Supported Types
| Type | Example | Notes |
|---|---|---|
string |
'' |
Text values |
int |
0 |
Integer numbers |
float |
0.0 |
Decimal numbers |
bool |
true |
Boolean flags |
array |
[] |
Arrays and associative arrays |
?type |
null |
Any type prefixed with ? for nullable |
Type Casting with $casts
When the client sends values back to the server (e.g., from form inputs), everything arrives as a string. LiVue resolves types using this priority: the $casts array first, then the PHP type hint via reflection, and finally no cast at all.
class Settings extends Component
{
public $count = 0;
public $price = 0.0;
public $enabled = false;
public $tags = '[]';
protected array $casts = [
'count' => 'int',
'price' => 'float',
'enabled' => 'bool',
'tags' => 'json',
];
}
Tip — When you use PHP type hints (e.g., public int $count), LiVue casts automatically via reflection. The $casts array is only needed for untyped properties or when you need json casting.
Actions
Public methods on your component become actions that can be called from templates. When an action is triggered (e.g., via v-click), LiVue sends an AJAX request to the server, executes the method, and returns the updated state and HTML.
class TodoList extends Component
{
public array $items = [];
public string $newItem = '';
public function add(): void
{
$this->items[] = $this->newItem;
$this->newItem = '';
}
public function remove(int $index): void
{
unset($this->items[$index]);
$this->items = array_values(
$this->items
);
}
}
<div>
<input v-model="newItem"
@keyup.enter="add()" />
<button v-click="add">Add</button>
<ul>
<li v-for="(item, i) in items">
{{ item }}
<button v-click="['remove', i]">
x
</button>
</li>
</ul>
</div>
Method Protection
Not all methods are callable from the client. LiVue uses reflection to automatically protect sensitive methods. The following are blocked from client-side invocation:
| Category | Details |
|---|---|
| Component base methods | Any method declared on LiVue\Component (render, getState, etc.) |
| Lifecycle hooks | boot, mount, hydrate, dehydrate, updating, updated, rendering, rendered |
| Magic methods | Any method prefixed with __ |
| Non-public methods | Protected and private methods |
Note — Only public methods declared directly on your concrete component class are callable from the client. Methods inherited from the base Component class are automatically blocked.
Lifecycle Hooks
LiVue provides lifecycle hooks like boot(), mount(), hydrate(), updating(), and more. These are optional methods you define on your component class to run code at specific moments.
For a comprehensive guide covering instance hooks, property-specific hooks, trait-based hooks, lifecycle events, and observers, see the dedicated Lifecycle page.
Page Components
A page component is a LiVue component rendered as a full page inside a Blade layout. Use LiVue::route() to map a URL to a component class.
use LiVue\Facades\LiVue;
LiVue::route('/dashboard', App\LiVue\Dashboard::class);
// With Laravel route chaining
LiVue::route('/users/{id}', App\LiVue\UserProfile::class)
->name('user.profile')
->middleware('auth');
Layout Resolution
LiVue resolves the layout in this priority order:
#[Layout]attribute on the class$layoutproperty on the componentconfig('livue.layout')from config file- Fallback:
components.layouts.app
Title Resolution
The page title follows a similar priority:
#[Title]attribute on the class$titleproperty on the componentconfig('app.name')
use LiVue\Attributes\Layout;
use LiVue\Attributes\Title;
#[Layout('layouts.dashboard')]
#[Title('User Profile')]
class UserProfile extends Component
{
public string $name = '';
public function mount(int $id): void
{
$user = User::findOrFail($id);
$this->name = $user->name;
}
protected function render(): string
{
return 'livue.user-profile';
}
}
class UserProfile extends Component
{
protected ?string $layout =
'layouts.dashboard';
protected ?string $title =
'User Profile';
public string $name = '';
public function mount(int $id): void
{
$user = User::findOrFail($id);
$this->name = $user->name;
}
protected function render(): string
{
return 'livue.user-profile';
}
}
Layout Template
Your layout must render the component HTML using unescaped output. Generate a layout with php artisan livue:layout.
<!DOCTYPE html>
<html>
<head>
<title>{{ $title ?? config('app.name') }}</title>
@livueHead
@vite(['resources/js/app.js'])
</head>
<body>
{!! $slot !!}
</body>
</html>
Note — Always use {!! $slot !!} (unescaped) because $slot contains rendered HTML from the component.
Head Management
Page components can inject dynamic elements into the <head> — meta tags, Open Graph, Twitter Cards, canonical URLs, and JSON-LD structured data. Use a $head property for static values or a head() method for dynamic values.
class About extends Component
{
protected array $head = [
'description' => 'About our company',
'og:title' => 'About Us',
'og:image' => 'https://example.com/about.jpg',
'twitter:card' => 'summary',
];
}
class BlogPost extends Component
{
public Post $post;
protected function head(): array
{
return [
'description' => $this->post->excerpt,
'og:title' => $this->post->title,
'og:description' => $this->post->excerpt,
'og:image' => $this->post->featured_image,
'twitter:card' => 'summary_large_image',
'canonical' => route('blog.show', $this->post),
'json-ld' => [
'@context' => 'https://schema.org',
'@type' => 'Article',
'name' => $this->post->title,
],
];
}
}
Supported Keys
| Key | Generated Tag |
|---|---|
description, robots, keywords, ... |
<meta name="..." content="..."> |
og:*, article:* |
<meta property="..." content="..."> |
twitter:* |
<meta name="twitter:..." content="..."> |
canonical |
<link rel="canonical" href="..."> |
json-ld |
<script type="application/ld+json">...</script> |
Note — Your layout must include the @livueHead directive in the <head> section (see the layout example above). All generated elements are marked with data-livue-head and are automatically updated during SPA navigation.
Priority
If both $head and head() are defined, the method takes priority over the property.
Nesting Components
LiVue components can be nested inside other components. Child components are rendered server-side and get their own independent Vue instance on the client. There are two equivalent syntaxes for including child components.
Inclusion Syntaxes
<div>
@livue('child-component')
@livue('child-component', [
'items' => $items,
'title' => $title
])
</div>
<div>
<livue:child-component />
<livue:child-component
:items="$items"
:title="$title" />
</div>
Passing Props via mount()
Child components receive props through their mount() method:
class ChildComponent extends Component
{
public array $items = [];
public string $title = '';
public function mount(array $items = [], string $title = ''): void
{
$this->items = $items;
$this->title = $title;
}
}
Reactive Props with #[Reactive]
Mark a child property with #[Reactive] to automatically sync it when the parent re-renders:
use LiVue\Attributes\Reactive;
class ItemList extends Component
{
#[Reactive]
public array $items = [];
// $items auto-updates when
// parent changes
}
<div>
<button v-click="addItem">
Add Item
</button>
@livue('item-list', [
'items' => $items
])
</div>
Communication Patterns
Child to Parent: dispatch()
Children communicate with parents by dispatching events:
class Modal extends Component
{
public function close(): void
{
$this->dispatch('modal-closed');
}
}
use LiVue\Attributes\On;
class Dashboard extends Component
{
#[On('modal-closed')]
public function onClosed(): void
{
// Handle modal closed
}
}
Parent to Child: Refs
Parents can call methods or set properties on child components using refs:
<div>
<button @click="livue.refs.myModal.call('open')">
Open Modal
</button>
@livue('modal', [], ['ref' => 'myModal'])
<livue:modal ref="myModal" />
</div>
| Ref API | Description |
|---|---|
livue.refs.name.call(method, params) |
Call a method on the child |
livue.refs.name.set(prop, value) |
Set a property on the child |
livue.refs.name.dispatch(event, data) |
Dispatch an event from the child |
livue.refs.name.sync() |
Sync child state with server |
livue.refs.name.state |
Access child reactive state (read-only) |
Single & Multi File Components
Beyond the standard class-based approach (separate PHP class + Blade template), LiVue supports two additional component styles: Single File Components (SFC) and Multi File Components (MFC).
Single File Component (SFC)
An SFC packs the PHP class and template into a single .blade.php file using an anonymous class. Ideal for simple, self-contained components.
<?php
use LiVue\Component;
new class extends Component {
public int $count = 0;
public function increment(): void
{
$this->count++;
}
};
?>
<div>
<p>Count: {{ count }}</p>
<button v-click="increment">+1</button>
</div>
SFC rules:
- -- The file must start with
<?php - -- The class must be anonymous:
new class extends Component - -- The PHP block must end with
?> - -- The HTML template follows after the closing PHP tag
- -- All LiVue features are supported (
#[Computed],#[Validate],@script, etc.)
Tip — Generate an SFC with php artisan make:livue MyComponent --single
Multi File Component (MFC)
An MFC splits the component into separate files within a folder. The file names must match the folder name (kebab-case).
resources/views/livue/my-widget/
|-- my-widget.php # Required: anonymous class
|-- my-widget.blade.php # Required: template
|-- my-widget.js # Optional: Vue Composition API
|-- my-widget.css # Optional: scoped styles
The JS file content is automatically wrapped in a @script block. The CSS file is automatically scoped to the component using a
data-livue-scope-{name} attribute selector prefixed to all rules.
.my-widget {
padding: 1rem;
}
.my-widget button {
color: blue;
}
[data-livue-scope-my-widget] .my-widget {
padding: 1rem;
}
[data-livue-scope-my-widget] .my-widget button {
color: blue;
}
Tip — Generate an MFC with php artisan make:livue MyComponent --multi
Resolution Order
When LiVue resolves a component by name, it searches in this order:
| Priority | Type | Location |
|---|---|---|
| 1 | Registered | LiVue::register() |
| 2 | Class-based | App\LiVue\Name |
| 3 | Single File | resources/views/livue/name.blade.php |
| 4 | Multi File | resources/views/livue/name/ |
| Aspect | Class-based | SFC | MFC |
|---|---|---|---|
| Files | 2 (PHP + Blade) | 1 | 2-4 |
| CSS Scoping | No | No | Yes (automatic) |
| Separate JS | No | No (@script inline) | Yes |
| Best for | Complex components | Simple components | Components with dedicated styling |
Islands
Islands are independent interactive regions that mount as separate Vue application instances, completely isolated from the parent component's Vue tree. Each island has its own state, reactivity, and AJAX updates.
When to Use Islands
- -- Heavy components: Widgets with complex local state
- -- Third-party integrations: Components that might conflict with parent reactivity
- -- Performance isolation: Prevent large re-renders from affecting other parts of the page
- -- State isolation: When you need completely independent state management
Declaring an Island
use LiVue\Attributes\Island;
#[Island]
class AnalyticsWidget extends Component
{
public array $data = [];
public function refresh(): void
{
$this->data =
AnalyticsService::getData();
}
}
class AnalyticsWidget extends Component
{
protected bool $island = true;
public array $data = [];
public function refresh(): void
{
$this->data =
AnalyticsService::getData();
}
}
How Islands Work
- -- Separate Vue app: Each island creates its own Vue application instance
- -- Independent state: Island state is completely isolated from the parent
- -- Independent updates: AJAX updates to an island do not affect the parent
- -- Own reactivity tree: Vue reactivity does not cross island boundaries
Combining with Lazy Loading
Islands work well with the #[Lazy] attribute for deferred, isolated components:
use LiVue\Attributes\Island;
use LiVue\Attributes\Lazy;
#[Island]
#[Lazy]
class HeavyWidget extends Component
{
// Loads on intersection AND has its own Vue app
}
Tip — Nested islands are supported (islands within islands). Events dispatched from islands can still be caught by parent components using the global event bus.
Lazy Loading
Lazy-loaded components defer their rendering until they enter the viewport (using IntersectionObserver) or after the page loads. The server skips mount() during the initial render and only executes it when the component is actually needed.
The #[Lazy] Attribute
use LiVue\Attributes\Lazy;
#[Lazy]
class LazyChart extends Component
{
public array $data = [];
public function placeholder(): string
{
return 'livue.chart-skeleton';
}
public function mount(): void
{
// Expensive query runs only when visible
$this->data = DB::table('analytics')
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
->groupBy('date')
->get()->toArray();
}
protected function render(): string
{
return 'livue.lazy-chart';
}
}
Options
| Usage | Behavior |
|---|---|
#[Lazy] |
Default: loads when the component enters the viewport |
#[Lazy(onLoad: true)] |
Loads after the page finishes loading (requestAnimationFrame) |
Placeholder
Define a placeholder() method to show a custom loading skeleton. It returns a Blade view name that will be displayed while the component loads. If not defined, LiVue uses its default placeholder.
<div class="h-72 bg-gray-100 rounded-lg flex items-center justify-center">
<span class="text-gray-400">Loading chart...</span>
</div>
How It Works
During the initial page render, the server does not mount the component. Instead, it outputs a <livue-lazy> wrapper with the placeholder content. On the client:
- The
<livue-lazy>Vue component mounts IntersectionObservermonitors the element (orrequestAnimationFrameifonLoad: true)- When triggered, a pooled AJAX request is sent to
/livue/update - The server mounts the component, executes
mount(), and returns HTML + snapshot - The client creates the Vue component and replaces the placeholder
Request Pooling
If multiple lazy components enter the viewport in the same microtask, their load requests are batched into a single HTTP request. For example, 5 components becoming visible simultaneously result in just one AJAX call.
Tip — LiVue includes a MutationObserver that automatically detects lazy components added to the DOM dynamically (e.g., via Turbo or HTMX), so they work seamlessly in any rendering context.
Macros
The base Component class includes Laravel's Macroable trait, allowing you to register dynamic methods at runtime via closures. Macros are callable from both PHP and the client (AJAX).
Registering Macros
Register macros in a service provider's boot() method. Macros have access to $this (the component instance), just like regular methods.
use LiVue\Component;
public function boot(): void
{
Component::macro('greet', function () {
return 'Hello, ' . $this->name;
});
Component::macro('setDefaults', function (string $name, int $count) {
$this->name = $name;
$this->count = $count;
});
}
Calling from the Client
Registered macros are automatically callable from the frontend via v-click, direct calls, and all other action directives:
<button v-click="greet">Greet</button>
<button v-click="['setDefaults', 'Test', 5]">Set Defaults</button>
Mixins
Group related macros into a class and register them all at once with mixin():
class DateHelpers
{
public function formatDate()
{
return function (string $field) {
return $this->{$field}->format('d/m/Y');
};
}
}
Component::mixin(new DateHelpers());
Security
Macros follow the same security rules as regular methods:
| Rule | Description |
|---|---|
| Magic methods | Names prefixed with __ are blocked |
| Lifecycle hooks | boot, mount, hydrate, etc. are blocked |
| Trait utilities | macro, hasMacro, flushMacros, mixin are not callable from client |
| Method precedence | Real methods always take precedence over macros with the same name |
Utility Methods
Component::hasMacro('greet'); // true
Component::flushMacros(); // Remove all macros