HTTP Requests with $action
The $action magic is the standard way to send HTTP requests from Alpine components in Gale.
It serializes state, includes CSRF protection, and applies server responses with SSE-style updates —
without leaving your page.
v2.0 Breaking Change: x-sync is Required
x-sync to the element with x-data (or use include per request) to send data.
The $action Magic
$action is the primary HTTP magic with automatic CSRF protection. It supports method chaining for different HTTP verbs:
| Syntax | HTTP Method | CSRF Protected | Use Case |
|---|---|---|---|
$action(url) |
POST | Yes | Default - most common pattern |
$action.get(url) |
GET | N/A | Read data, navigation |
$action.post(url) |
POST | Yes | Create resources (explicit) |
$action.put(url) |
PUT | Yes | Full resource replacements |
$action.patch(url) |
PATCH | Yes | Partial resource updates |
$action.delete(url) |
DELETE | Yes | Remove resources |
Default Behavior
$action(url) defaults to POST with CSRF protection - the most common pattern for Laravel applications.
Automatic CSRF Protection
Laravel requires CSRF tokens for state-changing requests. The $action magic
automatically handles CSRF protection for you:
- Automatically injects the CSRF token from your page's meta tag
- Includes credentials for session handling
- Handles token refresh when sessions expire
<!-- Your layout should include the CSRF meta tag -->
<meta name="csrf-token" content="rlomJbab7QGq1oAlOH6njxVUUdtJKqBLruLpuKfH">
<!-- $action automatically includes CSRF token -->
<button @click="$action('/save')">Save</button>
<!-- Your layout should include the CSRF meta tag -->
<meta name="csrf-token" content="rlomJbab7QGq1oAlOH6njxVUUdtJKqBLruLpuKfH">
<!-- $action automatically includes CSRF token -->
<button @click="$action('/save')">Save</button>
Basic Usage
$action takes a URL and an optional options object. Gale only sends state when you
opt in with x-sync (component-wide) or include (per request).
<div x-data="{ count: 0 }" x-sync>
<!-- GET request - for reading data -->
<button @click="$action.get('/refresh')">Refresh</button>
<!-- POST request with CSRF (default) -->
<button @click="$action('/increment')">Increment</button>
<span x-text="count"></span>
</div>
<div x-data="{ count: 0 }" x-sync>
<!-- GET request - for reading data -->
<button @click="$action.get('/refresh')">Refresh</button>
<!-- POST request with CSRF (default) -->
<button @click="$action('/increment')">Increment</button>
<span x-text="count"></span>
</div>
The controller receives the state in the request body:
public function increment(Request $request)
{
$count = $request->input('count', 0);
return gale()->state('count', $count + 1);
}
public function increment(Request $request)
{
$count = $request->input('count', 0);
return gale()->state('count', $count + 1);
}
Filtering State
By default, no state is sent. Use x-sync for component-wide sync,
or include/exclude per request
to control exactly what gets serialized.
Include (Whitelist)
Send only specific properties for this request:
<div x-data="{
name: '',
email: '',
password: '',
confirmPassword: '',
uiState: 'form'
}" x-sync>
<!-- Only send name and email -->
<button @click="$action('/save', { include: ['name', 'email'] })">
Save Profile
</button>
</div>
<div x-data="{
name: '',
email: '',
password: '',
confirmPassword: '',
uiState: 'form'
}" x-sync>
<!-- Only send name and email -->
<button @click="$action('/save', { include: ['name', 'email'] })">
Save Profile
</button>
</div>
Exclude (Blacklist)
Send everything except specified properties:
<div x-data="{
formData: { name: '', email: '' },
isLoading: false,
error: null,
showModal: false
}" x-sync>
<!-- Send formData but exclude UI state -->
<button @click="$action('/submit', { exclude: ['isLoading', 'error', 'showModal'] })">
Submit
</button>
</div>
<div x-data="{
formData: { name: '', email: '' },
isLoading: false,
error: null,
showModal: false
}" x-sync>
<!-- Send formData but exclude UI state -->
<button @click="$action('/submit', { exclude: ['isLoading', 'error', 'showModal'] })">
Submit
</button>
</div>
Browser-Only State (Underscore Prefix)
Properties prefixed with an underscore (_) are automatically excluded
from serialization. This is perfect for UI state that should never be sent to the server.
<div x-data="{
// These ARE sent to server
name: '',
email: '',
// These are NOT sent (underscore prefix)
_isEditing: false,
_selectedTab: 'general',
_tooltipVisible: false
}">
<!-- Only name and email are sent -->
<button @click="$action('/save')">Save</button>
</div>
<div x-data="{
// These ARE sent to server
name: '',
email: '',
// These are NOT sent (underscore prefix)
_isEditing: false,
_selectedTab: 'general',
_tooltipVisible: false
}" x-sync>
<!-- Only name and email are sent -->
<button @click="$action('/save')">Save</button>
</div>
Including Component State
Use includeComponents to send state from named components registered
with the x-component directive. This is powerful for cross-component
communication.
<!-- Cart component in header -->
<div x-component="cart" x-data="{ items: [], total: 0 }" x-sync>
...
</div>
<!-- Checkout form elsewhere -->
<div x-data="{ shipping: '', billing: '' }" x-sync>
<!-- Include cart state in checkout submission -->
<button @click="$action('/checkout', {
includeComponents: ['cart']
})">
Place Order
</button>
</div>
<!-- Cart component in header -->
<div x-component="cart" x-data="{ items: [], total: 0 }" x-sync>
...
</div>
<!-- Checkout form elsewhere -->
<div x-data="{ shipping: '', billing: '' }" x-sync>
<!-- Include cart state in checkout submission -->
<button @click="$action('/checkout', {
includeComponents: ['cart']
})">
Place Order
</button>
</div>
The server receives the component state under _components:
public function checkout(Request $request)
{
// Local component state
$shipping = $request->input('shipping');
// Cart component state
$cartItems = $request->input('_components.cart.items');
$cartTotal = $request->input('_components.cart.total');
// Process order...
}
public function checkout(Request $request)
{
// Local component state
$shipping = $request->input('shipping');
// Cart component state
$cartItems = $request->input('_components.cart.items');
$cartTotal = $request->input('_components.cart.total');
// Process order...
}
Selective Component Inclusion
You can include specific properties from components using an object format:
// Include only specific properties from cart
$action('/checkout', {
includeComponents: {
'cart': ['items', 'total'] // Only these properties
}
})
// Exclude sensitive properties
$action('/save', {
includeComponents: {
'user': { exclude: ['password', 'token'] }
}
})
// Rename component in request
$action('/submit', {
includeComponents: {
'cart': { as: 'shopping_cart' }
}
})
// Include only specific properties from cart
$action('/checkout', {
includeComponents: {
'cart': ['items', 'total'] // Only these properties
}
})
// Exclude sensitive properties
$action('/save', {
includeComponents: {
'user': { exclude: ['password', 'token'] }
}
})
// Rename component in request
$action('/submit', {
includeComponents: {
'cart': { as: 'shopping_cart' }
}
})
Include Components by Tag
Include all components with a specific tag using includeComponentsByTag:
<!-- Form field components with 'form-field' tag -->
<div x-component="name-field" data-tags="form-field" x-data="{ value: '' }" x-sync>...</div>
<div x-component="email-field" data-tags="form-field" x-data="{ value: '' }" x-sync>...</div>
<!-- Include all form-field components -->
<button @click="$action('/submit', {
includeComponentsByTag: ['form-field']
})">
Submit
</button>
<!-- Form field components with 'form-field' tag -->
<div x-component="name-field" data-tags="form-field" x-data="{ value: '' }" x-sync>...</div>
<div x-component="email-field" data-tags="form-field" x-data="{ value: '' }" x-sync>...</div>
<!-- Include all form-field components -->
<button @click="$action('/submit', {
includeComponentsByTag: ['form-field']
})">
Submit
</button>
Request Options
All HTTP magics accept these options:
| Option | Type | Default | Description |
|---|---|---|---|
include |
string[] | null | Whitelist of properties to send |
exclude |
string[] | [] | Blacklist of properties to skip |
includeComponents |
string[] | object | null | Named components to include |
includeComponentsByTag |
string[] | null | Tags to include components from |
headers |
object | {} | Additional HTTP headers |
retryInterval |
number | 1000 | Initial retry interval (ms) |
retryScaler |
number | 2 | Exponential backoff multiplier |
retryMaxWaitMs |
number | 30000 | Max retry wait time (ms) |
retryMaxCount |
number | 10 | Max retry attempts |
Custom Headers
Add custom headers to requests:
<button @click="$action('/api/resource', {
headers: {
'X-Custom-Header': 'value',
'Accept-Language': 'en-US'
}
})">
Request with custom headers
</button>
<button @click="$action('/api/resource', {
headers: {
'X-Custom-Header': 'value',
'Accept-Language': 'en-US'
}
})">
Request with custom headers
</button>
Automatic Features
Automatic Serialization
HTTP magics automatically handle complex data types:
- Arrays - Serialized as JSON arrays
- Objects - Serialized as JSON objects (nested supported)
- Dates - Converted to ISO 8601 strings
- Maps - Converted to plain objects
- Sets - Converted to arrays
- Functions - Automatically excluded
- DOM Elements - Automatically excluded
- Alpine internals - Automatically excluded (
$and_prefix)
Automatic Retry
Requests automatically retry on network failures using exponential backoff. The default configuration retries up to 10 times with increasing delays.
// Custom retry configuration
$action('/important-action', {
retryInterval: 500, // Start with 500ms
retryScaler: 1.5, // 1.5x each retry
retryMaxWaitMs: 10000, // Max 10 seconds
retryMaxCount: 5 // Only 5 attempts
})
// Custom retry configuration
$action('/important-action', {
retryInterval: 500, // Start with 500ms
retryScaler: 1.5, // 1.5x each retry
retryMaxWaitMs: 10000, // Max 10 seconds
retryMaxCount: 5 // Only 5 attempts
})
Automatic File Detection
When file inputs with the x-files directive contain files, HTTP magics
automatically switch from JSON to FormData, enabling seamless file uploads.
<div x-data="{ title: '' }" x-sync>
<input type="text" x-name="title">
<input type="file" name="document" x-files>
<!-- Automatically uses FormData when files are selected -->
<button @click="$action('/upload')">Upload</button>
</div>
<div x-data="{ title: '' }" x-sync>
<input type="text" x-name="title">
<input type="file" name="document" x-files>
<!-- Automatically uses FormData when files are selected -->
<button @click="$action('/upload')">Upload</button>
</div>
Complete Example
<form
x-data="{
name: '',
email: '',
_showSuccess: false
}"
x-sync
@submit.prevent="$action('/profile')"
>
<input type="text" x-name="name" placeholder="Name" required>
<div x-message="name" class="error"></div>
<input type="email" x-name="email" placeholder="Email" required>
<div x-message="email" class="error"></div>
<button type="submit" x-loading.attr="disabled">
<span x-loading.remove>Save Profile</span>
<span x-loading class="hidden">Saving...</span>
</button>
<div x-show="_showSuccess" class="success">
Profile saved successfully!
</div>
</form>
<form
x-data="{
name: '',
email: '',
_showSuccess: false
}"
x-sync
@submit.prevent="$action('/profile')"
>
<input type="text" x-name="name" placeholder="Name" required>
<div x-message="name" class="error"></div>
<input type="email" x-name="email" placeholder="Email" required>
<div x-message="email" class="error"></div>
<button type="submit" x-loading.attr="disabled">
<span x-loading.remove>Save Profile</span>
<span x-loading class="hidden">Saving...</span>
</button>
<div x-show="_showSuccess" class="success">
Profile saved successfully!
</div>
</form>
// Controller
public function updateProfile(Request $request)
{
$validated = $request->validateState([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
$request->user()->update($validated);
return gale()
->state('_showSuccess', true)
->clearMessages();
}
// Controller
public function updateProfile(Request $request)
{
$validated = $request->validateState([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
$request->user()->update($validated);
return gale()
->state('_showSuccess', true)
->clearMessages();
}