Component Operations
Target specific components by name and update their state or invoke their methods directly from the server.
Updating Component State
Use componentState() to update a specific component's Alpine state from the backend.
The component must be registered with the x-component directive.
// Controller
public function addToCart(Request $request)
{
$product = Product::find($request->input('product_id'));
// Add to user's cart...
$cart = $request->user()->cart();
$cart->add($product);
// Update the cart component directly
return gale()
->componentState('cart', [
'items' => $cart->items()->toArray(),
'total' => $cart->total(),
'count' => $cart->count(),
]);
}
// Controller
public function addToCart(Request $request)
{
$product = Product::find($request->input('product_id'));
// Add to user's cart...
$cart = $request->user()->cart();
$cart->add($product);
// Update the cart component directly
return gale()
->componentState('cart', [
'items' => $cart->items()->toArray(),
'total' => $cart->total(),
'count' => $cart->count(),
]);
}
The component on the frontend:
<!-- Cart component in header -->
<div x-data="{ items: [], total: 0, count: 0 }"
x-component="cart">
<span class="cart-badge">
<span x-text="count"></span>
</span>
<div class="cart-dropdown">
<template x-for="item in items">
<div x-text="item.name"></div>
</template>
<div class="total">Total: $<span x-text="total"></span></div>
</div>
</div>
<!-- Cart component in header -->
<div x-data="{ items: [], total: 0, count: 0 }"
x-component="cart">
<span class="cart-badge">
<span x-text="count"></span>
</span>
<div class="cart-dropdown">
<template x-for="item in items">
<div x-text="item.name"></div>
</template>
<div class="total">Total: $<span x-text="total"></span></div>
</div>
</div>
RFC 7386 Merge Semantics
State updates use RFC 7386 JSON Merge Patch semantics. This means updates merge
recursively with existing state, arrays are replaced entirely, and null
values delete properties.
// Current component state:
// { user: { name: 'John', email: 'john@example.com' }, items: [1, 2] }
return gale()->componentState('profile', [
'user' => [
'name' => 'Jane', // Updates name, keeps email
],
'items' => [3, 4, 5], // Replaces array entirely
]);
// Result: { user: { name: 'Jane', email: 'john@example.com' }, items: [3, 4, 5] }
// Current component state:
// { user: { name: 'John', email: 'john@example.com' }, items: [1, 2] }
return gale()->componentState('profile', [
'user' => [
'name' => 'Jane', // Updates name, keeps email
],
'items' => [3, 4, 5], // Replaces array entirely
]);
// Result: { user: { name: 'Jane', email: 'john@example.com' }, items: [3, 4, 5] }
Only If Missing
Use the onlyIfMissing option to initialize state without overwriting
existing values. This is useful for setting defaults when a component may already
have state from a previous interaction.
// Only set defaults if they don't already exist
return gale()->componentState('settings', [
'theme' => 'light',
'notifications' => true,
], [
'onlyIfMissing' => true,
]);
// Only set defaults if they don't already exist
return gale()->componentState('settings', [
'theme' => 'light',
'notifications' => true,
], [
'onlyIfMissing' => true,
]);
Invoking Component Methods
Use componentMethod() to call methods defined on a component's
x-data object. This allows the backend to trigger client-side
behavior like recalculating totals, refreshing data, or triggering animations.
// Controller - Invoke a method on a component
public function applyDiscount(Request $request)
{
$coupon = Coupon::where('code', $request->code)->first();
if (!$coupon) {
return gale()->messages(['coupon' => 'Invalid coupon code']);
}
// Tell the cart component to recalculate with the discount
return gale()->componentMethod('cart', 'applyDiscount', [
$coupon->percentage,
$coupon->code,
]);
}
// Controller - Invoke a method on a component
public function applyDiscount(Request $request)
{
$coupon = Coupon::where('code', $request->code)->first();
if (!$coupon) {
return gale()->messages(['coupon' => 'Invalid coupon code']);
}
// Tell the cart component to recalculate with the discount
return gale()->componentMethod('cart', 'applyDiscount', [
$coupon->percentage,
$coupon->code,
]);
}
The component with the target method:
<div x-component="cart"
x-data="{
items: [],
total: 0,
discount: 0,
couponCode: null,
applyDiscount(percentage, code) {
this.discount = percentage;
this.couponCode = code;
this.recalculate();
},
recalculate() {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
this.total = subtotal * (1 - this.discount / 100);
}
}">
<!-- Cart UI -->
<div x-show="couponCode" class="discount-badge">
<span x-text="discount + '% off with ' + couponCode"></span>
</div>
</div>
<div x-component="cart"
x-data="{
items: [],
total: 0,
discount: 0,
couponCode: null,
applyDiscount(percentage, code) {
this.discount = percentage;
this.couponCode = code;
this.recalculate();
},
recalculate() {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
this.total = subtotal * (1 - this.discount / 100);
}
}">
<!-- Cart UI -->
<div x-show="couponCode" class="discount-badge">
<span x-text="discount + '% off with ' + couponCode"></span>
</div>
</div>
Passing Arguments
Arguments are passed as an array and spread when the method is invoked. You can pass any JSON-serializable values: strings, numbers, booleans, arrays, and objects.
// No arguments
gale()->componentMethod('dashboard', 'refresh');
// Single argument
gale()->componentMethod('player', 'seek', [120]); // 2 minutes
// Multiple arguments
gale()->componentMethod('chart', 'updateData', [
[10, 20, 30, 40], // data array
'line', // chart type
['animate' => true] // options object
]);
// Complex object argument
gale()->componentMethod('notification', 'show', [[
'title' => 'Success!',
'message' => 'Your order has been placed.',
'type' => 'success',
'duration' => 5000,
]]);
// No arguments
gale()->componentMethod('dashboard', 'refresh');
// Single argument
gale()->componentMethod('player', 'seek', [120]); // 2 minutes
// Multiple arguments
gale()->componentMethod('chart', 'updateData', [
[10, 20, 30, 40], // data array
'line', // chart type
['animate' => true] // options object
]);
// Complex object argument
gale()->componentMethod('notification', 'show', [[
'title' => 'Success!',
'message' => 'Your order has been placed.',
'type' => 'success',
'duration' => 5000,
]]);
Combining Operations
You can combine component operations with other Gale methods in a single response. This allows you to update multiple components, show messages, and manipulate the DOM all at once.
public function checkout(Request $request)
{
$order = $this->processOrder($request);
return gale()
// Update the cart component to empty state
->componentState('cart', [
'items' => [],
'total' => 0,
'count' => 0,
])
// Update the order tracker with the new order
->componentState('order-tracker', [
'orderId' => $order->id,
'status' => 'processing',
])
// Trigger the confetti animation
->componentMethod('celebration', 'showConfetti')
// Show a success message
->messages(['_success' => 'Order placed successfully!']);
}
public function checkout(Request $request)
{
$order = $this->processOrder($request);
return gale()
// Update the cart component to empty state
->componentState('cart', [
'items' => [],
'total' => 0,
'count' => 0,
])
// Update the order tracker with the new order
->componentState('order-tracker', [
'orderId' => $order->id,
'status' => 'processing',
])
// Trigger the confetti animation
->componentMethod('celebration', 'showConfetti')
// Show a success message
->messages(['_success' => 'Order placed successfully!']);
}
Practical Patterns
Live Notifications
Push notifications to a specific component from anywhere in your backend:
// Notification component
<div x-component="notifications"
x-data="{
items: [],
add(notification) {
this.items.push({
...notification,
id: Date.now()
});
// Auto-dismiss after duration
if (notification.duration) {
setTimeout(() => this.dismiss(notification.id), notification.duration);
}
},
dismiss(id) {
this.items = this.items.filter(n => n.id !== id);
}
}">
<template x-for="item in items" :key="item.id">
<div :class="'notification notification-' + item.type">
<span x-text="item.message"></span>
<button @click="dismiss(item.id)">×</button>
</div>
</template>
</div>
<!-- Notification component -->
<div x-component="notifications"
x-data="{
items: [],
add(notification) {
this.items.push({
...notification,
id: Date.now()
});
// Auto-dismiss after duration
if (notification.duration) {
setTimeout(() => this.dismiss(notification.id), notification.duration);
}
},
dismiss(id) {
this.items = this.items.filter(n => n.id !== id);
}
}">
<template x-for="item in items" :key="item.id">
<div :class="'notification notification-' + item.type">
<span x-text="item.message"></span>
<button @click="dismiss(item.id)">×</button>
</div>
</template>
</div>
// Push notification from backend
gale()->componentMethod('notifications', 'add', [[
'type' => 'success',
'message' => 'Payment received!',
'duration' => 5000,
]]);
// Push notification from backend
gale()->componentMethod('notifications', 'add', [[
'type' => 'success',
'message' => 'Payment received!',
'duration' => 5000,
]]);
Modal Control
Control modals from the server:
<!-- Modal component -->
<div x-component="modal"
x-data="{ open: false, title: '', content: '' }">
<div x-show="open" class="modal-overlay">
<div class="modal-content">
<h2 x-text="title"></h2>
<div x-html="content"></div>
<button @click="open = false">Close</button>
</div>
</div>
</div>
<!-- Modal component -->
<div x-component="modal"
x-data="{ open: false, title: '', content: '' }">
<div x-show="open" class="modal-overlay">
<div class="modal-content">
<h2 x-text="title"></h2>
<div x-html="content"></div>
<button @click="open = false">Close</button>
</div>
</div>
</div>
// Open modal with content from backend
public function showTerms()
{
return gale()->componentState('modal', [
'open' => true,
'title' => 'Terms of Service',
'content' => view('partials.terms')->render(),
]);
}
// Open modal with content from backend
public function showTerms()
{
return gale()->componentState('modal', [
'open' => true,
'title' => 'Terms of Service',
'content' => view('partials.terms')->render(),
]);
}
Live Search Results
Update search results while preserving UI state:
public function search(Request $request)
{
$results = Product::search($request->input('q'))
->take(20)
->get();
// Update results while keeping the search input focused
return gale()->componentState('search', [
'results' => $results->toArray(),
'loading' => false,
'totalCount' => $results->count(),
]);
}
public function search(Request $request)
{
$results = Product::search($request->input('q'))
->take(20)
->get();
// Update results while keeping the search input focused
return gale()->componentState('search', [
'results' => $results->toArray(),
'loading' => false,
'totalCount' => $results->count(),
]);
}
Error Handling
When a component or method is not found, Gale logs a warning but doesn't throw an error. This allows your application to gracefully handle cases where components haven't registered yet or have been removed from the DOM.
// Console warnings (not errors):
// [Gale] Component "cart" not found for method invocation
// [Gale] Method "refresh" not found on component "cart"
// Check existence before calling if needed:
if ($components.has('cart')) {
$components.invoke('cart', 'refresh');
}
// Console warnings (not errors):
// [Gale] Component "cart" not found for method invocation
// [Gale] Method "refresh" not found on component "cart"
// Check existence before calling if needed:
if ($components.has('cart')) {
$components.invoke('cart', 'refresh');
}
SSE Protocol
Component operations are sent via Server-Sent Events. Understanding the protocol can help with debugging.
// componentState() sends:
event: gale-patch-component
data: component cart
data: state {"items":[],"total":0}
// componentMethod() sends:
event: gale-invoke-method
data: component cart
data: method applyDiscount
data: args [20,"SAVE20"]
// componentState() sends:
event: gale-patch-component
data: component cart
data: state {"items":[],"total":0}
// componentMethod() sends:
event: gale-invoke-method
data: component cart
data: method applyDiscount
data: args [20,"SAVE20"]
Component State vs Global State
| Method | Use Case | Scope |
|---|---|---|
gale()->state() |
Shared data across all components | Global x-data |
gale()->componentState() |
Update a specific named component | Single component |
gale()->componentMethod() |
Trigger behavior on a component | Single component |
Use global state when data should be accessible everywhere (user info, theme, etc.). Use component operations when targeting specific UI elements that manage their own state.