dancycodes/gale 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

dancycodes/gale

最新稳定版本:v0.1.8

Composer 安装命令:

composer require dancycodes/gale

包简介

Laravel-native reactive frontends using Alpine Gale. Build dynamic UIs with Blade templates and Server-Sent Events.

README 文档

README

PHP Version Laravel Version Alpine.js License Documentation

Laravel Gale is a server-driven reactive framework for Laravel. It combines Server-Sent Events (SSE) with Alpine.js to enable real-time UI updates directly from your Blade templates—no JavaScript framework, no build complexity, no API layer.

📚 View Full Documentation →

This README documents both:

  • Laravel Gale — The PHP backend package
  • Alpine Gale — An Alpine.js frontend plugin (bundled with Laravel Gale)

Table of Contents

Quick Start

A complete counter in 15 lines:

routes/web.php:

Route::get('/counter', fn() => view('counter'));

Route::post('/increment', function () {
    return gale()->state('count', request()->state('count', 0) + 1);
});

resources/views/counter.blade.php:

<!DOCTYPE html>
<html>
    <head>
        @gale
    </head>
    <body>
        <div x-data="{ count: 0 }" x-sync>
            <span x-text="count"></span>
            <button @click="$action('/increment')">+</button>
        </div>
    </body>
</html>

Click the button. The count updates via SSE. No page reload, no JavaScript written.

Note: The x-sync directive tells Gale to include all Alpine state in requests. See State Synchronization for details.

💡 See the Quickstart Guide for a step-by-step tutorial.

Installation

composer require dancycodes/gale
php artisan gale:install

Add @gale to your layout's <head>:

<head>
    @gale
</head>

That's it! You're ready to use Gale.

The @gale directive outputs:

  • CSRF meta tag
  • Alpine.js (v3) with Morph plugin
  • Alpine Gale plugin

Existing Alpine.js Projects

Gale includes Alpine.js (v3) with the Morph plugin. If you already have Alpine.js in your project, remove it to prevent conflicts:

If using CDN:

<!-- Remove this -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>

If using npm/Vite:

// Remove these lines from resources/js/app.js:
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Then use @gale instead — it handles everything.

Using Additional Alpine Plugins

Gale exposes window.Alpine, so you can still add other Alpine plugins:

<head>
    @gale
    <script>
        document.addEventListener('alpine:init', () => {
            Alpine.plugin(yourPlugin);
        });
    </script>
</head>

Or in your bundled JavaScript:

// resources/js/app.js
import persist from '@alpinejs/persist';

// Alpine is available globally via @gale
window.Alpine.plugin(persist);

Optional: Publish Configuration

php artisan vendor:publish --tag=gale-config

📖 For detailed installation instructions, see the Installation Guide.

Architecture

Request/Response Flow

┌─────────────────────────────────────────────────────────────┐
│                         BROWSER                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ Alpine.js Component (x-data)                           │ │
│  │   State: { count: 0, user: {...} }                     │ │
│  └────────────────────────────────────────────────────────┘ │
│                           │                                  │
│                           │ $action('/increment')            │
│                           ▼                                  │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ HTTP Request                                           │ │
│  │   Headers: Gale-Request, X-CSRF-TOKEN                  │ │
│  │   Body: { count: 0, user: {...} }  (serialized state)  │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                      LARAVEL SERVER                          │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ Controller                                             │ │
│  │   $count = request()->state('count');                  │ │
│  │   return gale()->state('count', $count + 1);           │ │
│  └────────────────────────────────────────────────────────┘ │
│                           │                                  │
│                           ▼                                  │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ SSE Response (text/event-stream)                       │ │
│  │   event: gale-patch-state                              │ │
│  │   data: state {"count":1}                              │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ Alpine.js receives SSE, merges state via RFC 7386           │
│   State: { count: 1, user: {...} }                          │
│   UI reactively updates                                      │
└─────────────────────────────────────────────────────────────┘

RFC 7386 JSON Merge Patch

State updates follow RFC 7386:

Server Sends Current State Result
{ count: 5 } { count: 0, name: "John" } { count: 5, name: "John" }
{ name: null } { count: 0, name: "John" } { count: 0 }
{ user: { email: "new@x.com" } } { user: { name: "John", email: "old@x.com" } } { user: { name: "John", email: "new@x.com" } }
  • Values merge: Sent values replace existing values
  • Null deletes: Sending null removes the property
  • Deep merge: Nested objects merge recursively

Alpine.js Context Requirement

All Alpine Gale features require an Alpine.js context:

<!-- Works: Inside x-data -->
<div x-data="{ count: 0 }">
    <button @click="$action('/increment')">+</button>
</div>

<!-- Works: x-init provides context -->
<div x-init="$action.get('/load')">Loading...</div>

<!-- Fails: No Alpine context -->
<button @click="$action('/increment')">Broken</button>

📖 Learn more about the Architecture & Concepts.

Backend: Laravel Gale

The gale() Helper

Returns a singleton GaleResponse instance with fluent API:

return gale()
    ->state('count', 42)
    ->state('updated', now()->toISOString())
    ->messages(['success' => 'Saved!']);

The same instance accumulates events throughout the request, then streams them as SSE.

State Management

state()

Set state values to merge into Alpine component:

// Single key-value
gale()->state('count', 42);

// Multiple values
gale()->state([
    'count' => 42,
    'user' => ['name' => 'John', 'email' => 'john@example.com'],
]);

// Nested update (merges with existing)
gale()->state('user.email', 'new@example.com');

Options:

// Only set if key doesn't exist in component state
gale()->state('defaults', ['theme' => 'dark'], ['onlyIfMissing' => true]);
Option Type Default Description
onlyIfMissing bool false Only set if property doesn't exist

forget()

Remove state properties (sends null per RFC 7386):

// Single key
gale()->forget('tempData');

// Multiple keys
gale()->forget(['tempData', 'cache', 'draft']);

messages()

Set the messages state object (commonly used for validation):

gale()->messages([
    'email' => 'Invalid email address',
    'password' => 'Password too short',
]);

// Success pattern
gale()->messages(['_success' => 'Profile saved!']);

clearMessages()

Clear all messages (sends empty object):

gale()->clearMessages();

DOM Manipulation

view()

Render a Blade view and patch it into the DOM:

// Basic: morphs by matching element IDs
gale()->view('partials.user-card', ['user' => $user]);

// With selector and mode
gale()->view('partials.item', ['item' => $item], [
    'selector' => '#items-list',
    'mode' => 'append',
]);

// As web fallback for non-Gale requests
gale()->view('dashboard', $data, web: true);

Options:

Option Type Default Description
selector string null CSS selector for target element
mode string 'outer' DOM patching mode (see DOM Patching Modes)
useViewTransition bool false Enable View Transitions API
settle int 0 Delay (ms) before patching
limit int null Max elements to patch
scroll string null Auto-scroll target: 'top' or 'bottom'
show string null Scroll into viewport: 'top' or 'bottom'
focusScroll bool false Maintain focus scroll position

fragment()

Render only a named fragment from a Blade view:

gale()->fragment('todos', 'todo-items', ['todos' => $todos]);

// With options
gale()->fragment('todos', 'todo-items', ['todos' => $todos], [
    'selector' => '#todo-list',
    'mode' => 'morph',
]);

Define fragments in Blade:

<div id="todo-list">
    @fragment('todo-items')
    @foreach($todos as $todo)
        <div id="todo-{{ $todo->id }}">{{ $todo->title }}</div>
    @endforeach
    @endfragment
</div>

fragments()

Render multiple fragments at once:

gale()->fragments([
    [
        'view' => 'todos',
        'fragment' => 'todo-items',
        'data' => ['todos' => $todos],
        'options' => ['selector' => '#todo-list'],
    ],
    [
        'view' => 'todos',
        'fragment' => 'todo-count',
        'data' => ['count' => $todos->count()],
    ],
]);

html()

Patch raw HTML into the DOM:

gale()->html('<div id="content">New content</div>');

// With options
gale()->html('<li>New item</li>', [
    'selector' => '#list',
    'mode' => 'append',
]);

DOM Convenience Methods

// Server-driven state (replacement via initTree)
gale()->outer('#element', '<div id="element">Replaced</div>');   // DEFAULT mode
gale()->inner('#container', '<p>Inner content</p>');

// Client-preserved state (smart morphing via Alpine.morph)
gale()->outerMorph('#element', '<div id="element">Updated</div>');
gale()->innerMorph('#container', '<p>Morphed content</p>');

// Insertion modes
gale()->append('#list', '<li>Last</li>');
gale()->prepend('#list', '<li>First</li>');
gale()->before('#target', '<div>Before</div>');
gale()->after('#target', '<div>After</div>');

// Removal
gale()->remove('.deprecated');

// Viewport modifiers (optional third parameter)
gale()->append('#chat', $html, ['scroll' => 'bottom']);
gale()->outer('#form', $html, ['show' => 'top']);
gale()->outerMorph('#list', $html, ['focusScroll' => true]);
Method Mode State Handling
outer($selector, $html, $opts) outer Server-driven
inner($selector, $html, $opts) inner Server-driven
outerMorph($selector, $html, $opts) outerMorph Client-preserved
innerMorph($selector, $html, $opts) innerMorph Client-preserved
append($selector, $html, $opts) append New elements init
prepend($selector, $html, $opts) prepend New elements init
before($selector, $html, $opts) before New elements init
after($selector, $html, $opts) after New elements init
remove($selector) remove Cleanup

Backward Compatibility: replace() and morph() still work as aliases for outer() and outerMorph() respectively.

Blade Fragments

Fragments extract specific sections from Blade views without rendering the entire template.

Defining fragments:

{{-- resources/views/dashboard.blade.php --}}
<div class="dashboard">
    @fragment('stats')
    <div id="stats">
        <span>Users: {{ $userCount }}</span>
        <span>Orders: {{ $orderCount }}</span>
    </div>
    @endfragment

    @fragment('recent-orders')
    <ul id="recent-orders">
        @foreach($recentOrders as $order)
            <li>{{ $order->number }}</li>
        @endforeach
    </ul>
    @endfragment
</div>

Rendering fragments:

// Single fragment
gale()->fragment('dashboard', 'stats', [
    'userCount' => User::count(),
    'orderCount' => Order::count(),
]);

// Multiple fragments
gale()->fragments([
    ['view' => 'dashboard', 'fragment' => 'stats', 'data' => $statsData],
    ['view' => 'dashboard', 'fragment' => 'recent-orders', 'data' => $ordersData],
]);

Redirects

Full-page browser redirects with session flash support:

// Basic redirect
return gale()->redirect('/dashboard');

// With flash data
return gale()->redirect('/dashboard')
    ->with('message', 'Welcome back!')
    ->with(['key' => 'value', 'another' => 'data']);

// With validation errors and input
return gale()->redirect('/register')
    ->withErrors($validator)
    ->withInput();

GaleRedirect Methods

$redirect = gale()->redirect('/url');

// Flash data
$redirect->with('key', 'value');
$redirect->with(['key' => 'value']);
$redirect->withInput();
$redirect->withInput(['name', 'email']);
$redirect->withErrors($validator);

// URL modifiers
$redirect->back('/fallback');
$redirect->backOr('route.name', ['param' => 'value']);
$redirect->refresh(preserveQuery: true, preserveFragment: false);
$redirect->home();
$redirect->route('route.name', ['id' => 1]);
$redirect->intended('/default');
$redirect->forceReload(bypassCache: false);
Method Description
with($key, $value) Flash data to session
withInput($input) Flash form input for repopulation
withErrors($errors) Flash validation errors
back($fallback) Redirect to previous URL with fallback
backOr($route, $params) Back with named route fallback
refresh($query, $fragment) Reload current page
home() Redirect to root URL
route($name, $params) Redirect to named route
intended($default) Redirect to auth intended URL
forceReload($bypass) Hard reload via JavaScript

Streaming Mode

For long-running operations, stream events in real-time:

return gale()->stream(function ($gale) {
    $users = User::cursor();
    $total = User::count();
    $processed = 0;

    foreach ($users as $user) {
        $user->processExpensiveOperation();
        $processed++;

        // Sent immediately
        $gale->state('progress', [
            'current' => $processed,
            'total' => $total,
            'percent' => round(($processed / $total) * 100),
        ]);
    }

    $gale->state('complete', true);
    $gale->messages(['_success' => "Processed {$total} users"]);
});

Streaming features:

  • Events sent immediately as they're added
  • dd() and dump() output captured and displayed
  • Exceptions rendered with stack traces
  • Redirects work via JavaScript

Navigation

Trigger SPA navigation from the backend:

// Basic navigation (pushes to history)
gale()->navigate('/users');

// With navigation key
gale()->navigate('/users', 'main-content');

// Navigate with explicit merge control
gale()->navigateWith('/users', 'main', merge: true);

// Merge query params
gale()->navigateMerge(['page' => 2]);
gale()->navigateMerge(['sort' => 'name'], 'table');

// Clean navigation (no param merging)
gale()->navigateClean('/users');

// Keep only specific params
gale()->navigateOnly('/search', ['q', 'category']);

// Keep all except specific params
gale()->navigateExcept('/search', ['page', 'cursor']);

// Replace history instead of push
gale()->navigateReplace('/users');

// Update just query parameters
gale()->updateQueries(['sort' => 'name', 'order' => 'asc']);

// Clear specific query parameters
gale()->clearQueries(['filter', 'search']);

// Full page reload
gale()->reload();
Method Description
navigate($url, $key) Navigate to URL with optional key
navigateWith($url, $key, $merge) Navigate with explicit merge control
navigateMerge($params, $key) Merge query params into current URL
navigateClean($url) Navigate without merging params
navigateOnly($url, $keep) Keep only specified params
navigateExcept($url, $remove) Remove specified params
navigateReplace($url) Replace history entry
updateQueries($params) Update query params in place
clearQueries($keys) Remove query params
reload() Full page reload

Events and JavaScript

dispatch()

Dispatch custom DOM events:

// Window-level event
gale()->dispatch('user-updated', ['id' => $user->id]);

// Targeted to selector
gale()->dispatch('refresh', ['section' => 'cart'], [
    'selector' => '.shopping-cart',
]);

// With event options
gale()->dispatch('notification', ['message' => 'Saved!'], [
    'bubbles' => true,
    'cancelable' => true,
    'composed' => false,
]);

Listen in Alpine:

<div x-data @user-updated.window="handleUpdate($event.detail)"></div>
Option Type Default Description
selector string null Target element(s)
bubbles bool true Event bubbles up
cancelable bool true Event can be canceled
composed bool false Event crosses shadow DOM

js()

Execute JavaScript in the browser:

gale()->js('console.log("Hello from server")');

gale()->js('myApp.showNotification("Saved!")', [
    'autoRemove' => true,
]);
Option Type Default Description
autoRemove bool false Remove script element after execution

Component Targeting

Target specific named Alpine components:

componentState()

Update a component's state by name:

gale()->componentState('cart', [
    'items' => $cartItems,
    'total' => $total,
]);

// Only set if property doesn't exist
gale()->componentState('cart', ['currency' => 'USD'], [
    'onlyIfMissing' => true,
]);

componentMethod()

Invoke a method on a named component:

gale()->componentMethod('cart', 'recalculate');
gale()->componentMethod('form', 'reset');
gale()->componentMethod('calculator', 'setValues', [10, 20, 30]);

Request Macros

Laravel Gale registers these macros on the Request object:

isGale()

Check if the request is a Gale request:

if (request()->isGale()) {
    return gale()->state('data', $data);
}
return view('page', compact('data'));

state()

Access state sent from the Alpine component:

// All state
$state = request()->state();

// Specific key with default
$count = request()->state('count', 0);

// Nested value (dot notation)
$email = request()->state('user.email');

isGaleNavigate()

Check if request is a navigation request:

// Any navigate request
if (request()->isGaleNavigate()) {
    return gale()->fragment('page', 'content', $data);
}

// Specific key
if (request()->isGaleNavigate('sidebar')) {
    return gale()->fragment('page', 'sidebar', $data);
}

// Multiple keys (matches any)
if (request()->isGaleNavigate(['main', 'sidebar'])) {
    // ...
}

galeNavigateKey() / galeNavigateKeys()

Get the navigation key(s):

$key = request()->galeNavigateKey();     // 'sidebar' or null
$keys = request()->galeNavigateKeys();   // ['sidebar', 'main'] or []

validateState()

Validate state with automatic SSE error response:

$validated = request()->validateState([
    'email' => 'required|email',
    'name' => 'required|min:2',
], [
    'email.required' => 'Email is required',
]);

// On failure: throws GaleMessageException (SSE error response)
// On success: returns validated data, clears messages for validated fields

The validation uses selective clearing—only messages for validated fields are cleared, preserving other messages.

Blade Directives

@gale

Include the JavaScript bundle and CSRF meta tag:

<head>
    @gale
</head>

Outputs:

<meta name="csrf-token" content="..." />
<script type="module" src="/vendor/gale/js/gale.js"></script>

@fragment / @endfragment

Define extractable fragments:

@fragment('header')
<header>{{ $title }}</header>
@endfragment

@ifgale / @else / @endifgale

Conditional rendering based on request type:

@ifgale
    {{-- Gale request: partial content --}}
    <div id="content">{{ $content }}</div>
@else
    {{-- Regular request: full page --}}
    @include('layouts.app')
@endifgale

Validation

Using validateState Macro

public function store(Request $request)
{
    $validated = $request->validateState([
        'name' => 'required|min:2|max:255',
        'email' => 'required|email|unique:users',
    ]);

    User::create($validated);
    return gale()->messages(['_success' => 'Account created!']);
}

Using GaleMessageException

For custom validation flows:

use Dancycodes\Gale\Exceptions\GaleMessageException;

public function update(Request $request)
{
    $validator = Validator::make($request->state(), [
        'email' => 'required|email',
    ]);

    if ($validator->fails()) {
        throw new GaleMessageException($validator);
    }

    // Process...
}

Manual Message Handling

if ($validator->fails()) {
    return gale()->messages($validator->errors()->toArray());
}

// On success
return gale()->clearMessages();

Route Discovery

Optional attribute-based route discovery:

Enable in Configuration

// config/gale.php
return [
    'route_discovery' => [
        'enabled' => true,
        'discover_controllers_in_directory' => [
            app_path('Http/Controllers'),
        ],
        'discover_views_in_directory' => [
            'docs' => resource_path('views/docs'),
        ],
    ],
];

Controller Attributes

use Dancycodes\Gale\Routing\Attributes\Route;
use Dancycodes\Gale\Routing\Attributes\Prefix;
use Dancycodes\Gale\Routing\Attributes\Where;
use Dancycodes\Gale\Routing\Attributes\DoNotDiscover;
use Dancycodes\Gale\Routing\Attributes\WithTrashed;

#[Prefix('/admin')]
class UserController extends Controller
{
    #[Route('GET', '/users', name: 'admin.users.index')]
    public function index() { }

    #[Route('GET', '/users/{id}', name: 'admin.users.show')]
    #[Where('id', Where::NUMERIC)]
    #[WithTrashed]
    public function show($id) { }

    #[Route(['GET', 'POST'], '/users/search')]
    public function search() { }

    #[DoNotDiscover]
    public function internalMethod() { }
}

Route Attribute Options

Attribute Parameters Description
#[Route] methods, uri, name, middleware, domain, withTrashed Define route
#[Prefix] prefix URL prefix for class
#[Where] param, pattern Route parameter constraint
#[DoNotDiscover] Exclude from discovery
#[WithTrashed] Include soft-deleted models

Where Constants:

Constant Pattern
Where::ALPHA [a-zA-Z]+
Where::NUMERIC [0-9]+
Where::ALPHANUMERIC [a-zA-Z0-9]+
Where::UUID UUID pattern

📖 See the full Backend Reference for all available methods and options.

Frontend: Alpine Gale

All frontend features require an Alpine.js context (x-data or x-init).

HTTP Magics

The $action magic handles all HTTP requests to your Gale backend. It defaults to POST with automatic CSRF injection—the most common pattern for Gale actions.

Basic Usage

<div x-data="{ count: 0 }" x-sync>
    <!-- Default: POST with CSRF (most common) -->
    <button @click="$action('/increment')">+1</button>

    <!-- Method shorthands -->
    <button @click="$action.get('/api/data')">GET</button>
    <button @click="$action.post('/api/save')">POST</button>
    <button @click="$action.put('/api/replace')">PUT</button>
    <button @click="$action.patch('/api/update')">PATCH</button>
    <button @click="$action.delete('/api/remove')">DELETE</button>
</div>

CSRF Protection

CSRF tokens are automatically injected for all non-GET methods:

  • $action() → POST with CSRF
  • $action.post() → POST with CSRF
  • $action.put() → PUT with CSRF
  • $action.patch() → PATCH with CSRF
  • $action.delete() → DELETE with CSRF
  • $action.get() → GET (no CSRF needed)

You can also specify the method via options:

<button @click="$action('/search', { method: 'get' })">Search</button>

Request Options

<button
    @click="$action('/save', {
    include: ['user', 'settings'],
    exclude: ['tempData'],
    headers: { 'X-Custom': 'value' },
    retryInterval: 1000,
    retryScaler: 2,
    retryMaxWaitMs: 30000,
    retryMaxCount: 10,
    requestCancellation: true,
    onProgress: (percent) => console.log(percent)
})"
>
    Save
</button>
Option Type Default Description
method string 'POST' HTTP method (GET, POST, etc.)
include array null Only send these state keys
exclude array null Don't send these state keys
headers object {} Additional request headers
retryInterval number 1000 Initial retry delay (ms)
retryScaler number 2 Exponential backoff multiplier
retryMaxWaitMs number 30000 Maximum retry delay (ms)
retryMaxCount number 10 Maximum retry attempts
requestCancellation bool false Cancel previous request
onProgress function null Upload progress callback

State Synchronization (x-sync)

The x-sync directive controls which Alpine state properties are sent to the server with HTTP requests.

Basic Usage

<!-- Send everything (empty = wildcard) -->
<div x-data="{ name: '', email: '', open: false }" x-sync>

<!-- Send specific keys only -->
<div x-data="{ name: '', email: '', open: false }" x-sync="['name', 'email']">

<!-- String syntax shorthand -->
<div x-data="{ name: '', email: '' }" x-sync="name, email">

<!-- Explicit wildcard (same as empty) -->
<div x-data="{ name: '', email: '' }" x-sync="*">

<!-- No x-sync = send nothing automatically -->
<div x-data="{ name: '', temp: null }">

Behavior

x-sync Value Result
x-sync (empty) Send all state (wildcard)
x-sync="*" Send all state (explicit wildcard)
x-sync="['a','b']" Send only 'a' and 'b'
x-sync="a, b" Send only 'a' and 'b' (string syntax)
No directive Send nothing (use include option if needed)

Interaction with Request Options

The include and exclude options on HTTP magics work together with x-sync:

<!-- x-sync defines base, include adds more -->
<div x-data="{ a: 1, b: 2, c: 3 }" x-sync="['a']">
  <button @click="$action('/save', { include: ['c'] })">Save</button>
  <!-- Sends: { a: 1, c: 3 } -->
</div>

<!-- exclude always removes -->
<div x-data="{ a: 1, b: 2, c: 3 }" x-sync>
  <button @click="$action('/save', { exclude: ['b'] })">Save</button>
  <!-- Sends: { a: 1, c: 3 } -->
</div>
x-sync request include request exclude Result
(empty) - - {all state} (wildcard)
['a','b'] - - {a, b}
['a','b'] ['c'] - {a, b, c} (union)
['a','b'] - ['b'] {a}
* or (empty) ['a','b'] - {a, b} (include restricts wildcard)
(none) - - {} (nothing)
(none) ['name'] - {name}

Form Fields

Form fields are handled separately from x-sync:

  • Form fields with name attribute are always included by default
  • Use includeFormFields: false to exclude form fields
  • Alpine state overrides form fields on key conflicts
<form>
  <input name="email" value="form@example.com">
  <div x-data="{ email: 'alpine@example.com' }" x-sync="['email']">
    <button @click="$action('/save')">Save</button>
    <!-- Sends: { email: 'alpine@example.com' } (Alpine overrides form) -->
  </div>
</form>

CSRF Configuration

The @gale directive adds <meta name="csrf-token">. The $action magic reads this token automatically for all non-GET requests.

Configuration

Alpine.gale.configureCsrf({
    headerName: "X-CSRF-TOKEN",
    metaName: "csrf-token",
    cookieName: "XSRF-TOKEN",
});

// Get current config
const config = Alpine.gale.getCsrfConfig();
Option Default Description
headerName 'X-CSRF-TOKEN' Header name for token
metaName 'csrf-token' Meta tag name to read
cookieName 'XSRF-TOKEN' Cookie name as fallback

Global State ($gale)

The $gale magic provides global connection state:

<div x-data>
    <div x-show="$gale.loading">Loading...</div>
    <div x-show="$gale.retrying">Reconnecting...</div>
    <div x-show="$gale.retriesFailed">Connection failed</div>

    <div x-show="$gale.error">
        Error: <span x-text="$gale.lastError"></span>
    </div>

    <span x-text="$gale.activeCount + ' requests active'"></span>

    <ul>
        <template x-for="err in $gale.errors">
            <li x-text="err"></li>
        </template>
    </ul>

    <button @click="$gale.clearErrors()">Clear Errors</button>
</div>
Property Type Description
loading bool Any request in progress
activeCount number Number of active requests
retrying bool Currently retrying a request
retriesFailed bool All retries exhausted
error bool Has any error
lastError string Most recent error message
errors array All error messages
clearErrors() function Clear all errors

Element State ($fetching)

The $fetching() magic function tracks per-element loading state:

<button @click="$action('/save')" :disabled="$fetching()">
    <span x-show="!$fetching()">Save</span>
    <span x-show="$fetching()">Saving...</span>
</button>

Note: $fetching is a function—always use $fetching() with parentheses. Set automatically when the element initiates a request.

Loading Directives

x-indicator

Creates a boolean state variable tracking requests within the element tree:

<div x-data="{ saving: false }" x-indicator="saving">
    <button @click="$action('/save')" :disabled="saving">
        <span x-show="!saving">Save</span>
        <span x-show="saving">Saving...</span>
    </button>
</div>

x-loading

Show/hide elements or apply classes during loading:

<!-- Show during loading -->
<div x-loading>Loading...</div>

<!-- Hide during loading -->
<div x-loading.remove>Content</div>

<!-- Add class during loading -->
<button x-loading.class="opacity-50">Submit</button>

<!-- Add attribute during loading -->
<button x-loading.attr="disabled">Submit</button>

<!-- Delay showing (prevents flicker for fast requests) -->
<div x-loading.delay.200ms>Loading...</div>
Modifier Description
.class Add class(es) during loading
.attr Add attribute during loading
.remove Hide element during loading
.delay.{ms} Delay before showing

Navigation

x-navigate Directive

Enable SPA navigation on links and forms:

<!-- Links -->
<a href="/users" x-navigate>Users</a>

<!-- Forms -->
<form action="/search" method="GET" x-navigate>
    <input name="q" type="text" />
    <button type="submit">Search</button>
</form>

<!-- Dynamic URL -->
<button x-navigate="'/users/' + userId">View</button>

Navigation Modifiers

<!-- Merge with current query params -->
<a href="/users?sort=name" x-navigate.merge>Sort</a>

<!-- Replace history instead of push -->
<a href="/users" x-navigate.replace>Users</a>

<!-- Navigation key for partial updates -->
<a href="/users" x-navigate.key.sidebar>Users</a>

<!-- Keep only specific params -->
<a href="/search?q=test" x-navigate.only.q.category>Search</a>

<!-- Keep all except specific params -->
<a href="/search" x-navigate.except.page>Reset Page</a>

<!-- Debounce (for inputs) -->
<input x-navigate.debounce.300ms="'/search?q=' + $el.value" />

<!-- Throttle -->
<button x-navigate.throttle.500ms="'/next'">Next</button>

<!-- Combined -->
<a href="/users?page=2" x-navigate.merge.key.pagination.replace>Page 2</a>
Modifier Description
.merge Merge query params with current
.replace Replace history entry
.key.{name} Navigation key for targeting
.only.{params} Keep only these params
.except.{params} Remove these params
.debounce.{ms} Debounce navigation
.throttle.{ms} Throttle navigation

$navigate Magic

<button @click="$navigate('/users')">Users</button>

<button
    @click="$navigate('/users', {
    merge: true,
    replace: true,
    key: 'main-content',
    only: ['q'],
    except: ['page']
})"
>
    Navigate
</button>

x-navigate-skip

Exclude specific links from navigation handling:

<nav x-navigate>
    <a href="/dashboard">Dashboard</a>
    <a href="/external" x-navigate-skip>External</a>
    <a href="/file.pdf" x-navigate-skip>Download</a>
</nav>

Component Registry

Named components that can be targeted from the backend or other components.

x-component Directive

<div x-data="{ items: [], total: 0 }" x-component="cart">
    <span x-text="total"></span>
</div>

<!-- With tags -->
<div x-data="{ count: 0 }" x-component="counter" data-tags="widgets,dashboard">
    <span x-text="count"></span>
</div>

$components Magic

<div x-data>
    <!-- Check existence -->
    <span x-show="$components.has('cart')">Cart loaded</span>

    <!-- Get component data -->
    <button @click="console.log($components.get('cart'))">Log Cart</button>

    <!-- Get all components -->
    <button @click="console.log($components.all())">Log All</button>

    <!-- Get by tag -->
    <button @click="$components.getByTag('widgets').forEach(c => c.refresh())">
        Refresh Widgets
    </button>

    <!-- Update state -->
    <button @click="$components.update('cart', { total: 0 })">
        Clear Cart
    </button>

    <!-- Create state (like state() but creates new) -->
    <button @click="$components.create('cart', { currency: 'EUR' })">
        Set Currency
    </button>

    <!-- Delete state keys -->
    <button @click="$components.delete('cart', ['tempItems'])">Clean Up</button>

    <!-- Invoke method -->
    <button @click="$components.invoke('cart', 'recalculate')">
        Recalculate
    </button>

    <!-- Reactive state access -->
    <span x-text="$components.state('cart', 'total')"></span>

    <!-- Watch for changes -->
    <div
        x-init="$components.watch('cart', 'total', (val) => console.log(val))"
    ></div>

    <!-- Wait for component -->
    <div x-init="$components.when('cart').then(c => console.log(c))"></div>

    <!-- Callback when ready -->
    <div x-init="$components.onReady('cart', (c) => console.log(c))"></div>
</div>
Method Description
get(name) Get component Alpine data object
getByTag(tag) Get all components with tag
all() Get all registered components
has(name) Check if component exists
invoke(name, method, ...args) Call method on component
when(name, timeout?) Promise resolving when component exists
onReady(name, callback) Callback when component ready
state(name, property) Get reactive state value
update(name, state) Merge state into component
create(name, state, options) Set state (with onlyIfMissing option)
delete(name, keys) Remove state keys
watch(name, property, callback) Watch for changes

$invoke Shorthand

<button @click="$invoke('cart', 'addItem', productId, qty)">Add</button>

Lifecycle Hooks

// When component registers
Alpine.gale.onComponentRegistered((name, component) => {
    console.log(`${name} registered`);
});

// When component unregisters
Alpine.gale.onComponentUnregistered((name) => {
    console.log(`${name} unregistered`);
});

// When component state changes
Alpine.gale.onComponentStateChanged((name, property, value) => {
    console.log(`${name}.${property} = ${value}`);
});

Form Binding (x-name)

The x-name directive simplifies form element binding by combining x-model behavior with automatic state creation and Laravel-compatible name attributes.

Basic Usage

<!-- Before: Verbose, requires pre-declaring state -->
<div x-data="{ email: '', password: '' }">
    <input x-model="email" name="email" type="email">
    <input x-model="password" name="password" type="password">
</div>

<!-- After: Clean and declarative -->
<div x-data="{ email: '', password: '' }">
    <input x-name="email" type="email">
    <input x-name="password" type="password">
</div>

The directive:

  • Creates two-way binding (like x-model)
  • Sets the name attribute automatically for FormData/Laravel compatibility
  • Auto-creates state if it doesn't exist in x-data
  • For file inputs, delegates to the x-files system

Type-Aware Default Values

When state is auto-created, appropriate defaults are used based on input type:

Input Type Default Value Notes
text, email, password, tel, url, search '' Empty string
number, range null Distinguishes "not entered" from 0
checkbox (single) false Boolean toggle
checkbox (array mode) [] Multiple selections
radio null No selection initially
select '' Empty selection
select[multiple] [] Array of selections
textarea '' Text content
file Uses x-files WeakMap registry

If the element has a value attribute, that value is used as the initial state:

<div x-data>
    <input x-name="count" type="number" value="42">
    <!-- State: { count: 42 } -->
</div>

Nested Paths

Use dot notation for nested state structures:

<div x-data="{ user: { name: '', email: '', phone: '' } }">
    <input x-name="user.name" type="text">
    <input x-name="user.email" type="email">
    <input x-name="user.phone" type="tel">
</div>

Deep nesting works too:

<div x-data="{ form: { contact: { address: { city: '' } } } }">
    <input x-name="form.contact.address.city" type="text">
</div>

Checkboxes

Single checkbox (boolean mode):

<div x-data="{ newsletter: false }">
    <input x-name="newsletter" type="checkbox">
    <!-- Toggles between true/false -->
</div>

Multiple checkboxes (array mode):

Use the .array modifier or let Gale auto-detect when multiple checkboxes share the same name:

<div x-data="{ tags: [] }">
    <!-- Explicit array mode with .array modifier -->
    <input x-name.array="tags" type="checkbox" value="alpha">
    <input x-name.array="tags" type="checkbox" value="beta">
    <input x-name.array="tags" type="checkbox" value="gamma">
    <!-- State: { tags: ['alpha', 'beta'] } when first two are checked -->
</div>

<div x-data="{ colors: [] }">
    <!-- Auto-detected array mode (multiple checkboxes with same x-name) -->
    <input x-name="colors" type="checkbox" value="red">
    <input x-name="colors" type="checkbox" value="green">
    <input x-name="colors" type="checkbox" value="blue">
</div>

Radio Buttons

<div x-data="{ gender: null }">
    <input x-name="gender" type="radio" value="male"> Male
    <input x-name="gender" type="radio" value="female"> Female
    <input x-name="gender" type="radio" value="other"> Other
    <!-- State: { gender: 'female' } when female selected -->
</div>

Select Elements

Single select:

<div x-data="{ country: '' }">
    <select x-name="country">
        <option value="">Choose...</option>
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
    </select>
</div>

Multiple select:

<div x-data="{ languages: [] }">
    <select x-name="languages" multiple>
        <option value="js">JavaScript</option>
        <option value="php">PHP</option>
        <option value="py">Python</option>
    </select>
    <!-- State: { languages: ['js', 'php'] } when both selected -->
</div>

Modifiers

Modifier Description
.lazy Update on blur instead of input
.number Parse value as number
.trim Trim whitespace
.array Force array mode for checkboxes

Examples:

<!-- Update only on blur (not every keystroke) -->
<input x-name.lazy="search" type="text">

<!-- Coerce to number -->
<input x-name.number="quantity" type="text">

<!-- Trim whitespace -->
<input x-name.trim="username" type="text">

<!-- Combine modifiers -->
<input x-name.lazy.trim="bio" type="text">

File Inputs

File inputs are automatically delegated to the x-files system:

<div x-data>
    <input x-name="avatar" type="file">
    <!-- Equivalent to: <input x-files="avatar" name="avatar" type="file"> -->

    <p x-show="$file('avatar')">
        Selected: <span x-text="$file('avatar')?.name"></span>
    </p>
</div>

Server Integration

Forms using x-name work seamlessly with Gale's HTTP magics:

<div x-data="{ firstName: '', lastName: '', response: '' }">
    <input x-name="firstName" type="text" placeholder="First name">
    <input x-name="lastName" type="text" placeholder="Last name">

    <button @click="$action('/api/greet')">Submit</button>

    <p x-text="response"></p>
</div>
Route::post('/api/greet', function (Request $request) {
    $firstName = $request->state('firstName');
    $lastName = $request->state('lastName');

    return gale()->state([
        'response' => "Hello, {$firstName} {$lastName}!"
    ]);
});

Integration with x-message

Field names from x-name map directly to Laravel validation error keys:

<div x-data="{ email: '' }">
    <input x-name="email" type="email">
    <span x-message="email" class="text-red-500"></span>

    <button @click="$action('/subscribe')">Subscribe</button>
</div>
Route::post('/subscribe', function (Request $request) {
    $request->validate([
        'state.email' => 'required|email'
    ]);

    // Process subscription...
});

File Uploads

x-files Directive

<div x-data>
    <input type="file" name="avatar" x-files />

    <div x-show="$file('avatar')">
        <p>Name: <span x-text="$file('avatar')?.name"></span></p>
        <p>Size: <span x-text="$formatBytes($file('avatar')?.size)"></span></p>
        <img :src="$filePreview('avatar')" />
    </div>

    <button @click="$action('/upload')">Upload</button>
</div>

Multiple Files

<input type="file" name="docs" x-files multiple />

<template x-for="(file, i) in $files('docs')" :key="i">
    <div>
        <span x-text="file.name"></span>
        <span x-text="$formatBytes(file.size)"></span>
        <img :src="$filePreview('docs', i)" />
    </div>
</template>

<button @click="$clearFiles('docs')">Clear</button>

Validation Modifiers

<!-- Max size (5MB) -->
<input type="file" x-files.max-size-5mb />

<!-- Max files -->
<input type="file" x-files.max-files-3 multiple />

<!-- Combined -->
<input type="file" x-files.max-size-10mb.max-files-5 multiple />

File Events

<div x-data @gale:file-change="console.log($event.detail)">
    <input type="file" x-files />
</div>

<div x-data @gale:file-error="alert($event.detail.message)">
    <input type="file" x-files.max-size-1mb />
</div>
Event Detail
gale:file-change { name, files }
gale:file-error { name, message, type }

File Magics

Magic Description
$file(name) Get single file info
$files(name) Get array of files
$filePreview(name, index?) Get preview URL
$clearFiles(name?) Clear file input(s)
$formatBytes(size, decimals?) Format bytes to human-readable
$uploading Upload in progress
$uploadProgress Progress 0-100
$uploadError Error message

Message Display

x-message Directive

Display messages from state:

<div x-data="{ messages: {} }">
    <input name="email" type="email" />
    <span x-message="email" class="text-red-500"></span>

    <input name="password" type="password" />
    <span x-message="password" class="text-red-500"></span>

    <div x-message="_success" class="text-green-500"></div>

    <button @click="$action('/login')">Login</button>
</div>

Message Path

Access nested message paths:

<span x-message="user.email"></span>

Array Validation with x-for

Use template literals to display validation errors for array items in loops:

<div x-data="{
    items: [
        { name: '', quantity: 1 },
        { name: '', quantity: 1 }
    ],
    messages: {}
}">
    <template x-for="(item, index) in items" :key="index">
        <div>
            <input x-model="items[index].name">
            <span x-message="`items.${index}.name`" class="text-red-500"></span>

            <input x-model="items[index].quantity" type="number">
            <span x-message="`items.${index}.quantity`" class="text-red-500"></span>
        </div>
    </template>

    <button @click="$action('/validate-items')">Validate</button>
</div>

Backend validation with validateState:

Route::post('/validate-items', function (Request $request) {
    $validated = $request->validateState([
        'items' => 'required|array|min:1',
        'items.*.name' => 'required|string|min:2',
        'items.*.quantity' => 'required|integer|min:1',
    ], [
        'items.*.name.required' => 'Item name is required',
        'items.*.name.min' => 'Item name must be at least 2 characters',
        'items.*.quantity.min' => 'Quantity must be at least 1',
    ]);

    return gale()->messages(['_success' => 'All items validated!']);
});

Supported expression syntaxes:

<!-- Template literals (recommended) -->
<span x-message="`items.${index}.name`"></span>

<!-- String concatenation -->
<span x-message="'items.' + index + '.name'"></span>

<!-- Nested arrays -->
<span x-message="`items.${i}.details.${j}.value`"></span>

Wildcard clearing: When using validateState with wildcard rules like items.*.name, all matching message keys (e.g., items.0.name, items.1.name) are automatically cleared before validation, ensuring stale errors from removed items don't persist.

Message Types

Messages can include type prefixes for styling:

gale()->messages([
    'email' => '[ERROR] Invalid email',
    'saved' => '[SUCCESS] Changes saved',
    'note' => '[WARNING] Session expiring',
    'info' => '[INFO] New features available',
]);

Auto-applied classes: message-error, message-success, message-warning, message-info.

Configuration

Alpine.gale.configureMessage({
    defaultStateKey: "messages",
    autoHide: true,
    autoShow: true,
    typeClasses: {
        success: "message-success",
        error: "message-error",
        warning: "message-warning",
        info: "message-info",
    },
});

Interval Execution

x-interval Directive

Runs Alpine expressions at configurable intervals. Like x-init, but repeating.

<!-- Basic: increment every second -->
<div x-data="{ count: 0 }" x-interval.1s="count++">
    <span x-text="count"></span>
</div>

<!-- HTTP polling using $get -->
<div x-data="{ status: '' }" x-interval.5s="$action.get('/api/status')">
    <span x-text="status"></span>
</div>

<!-- Multiple expressions -->
<div x-data="{ tick: 0 }" x-interval.2s="tick++; checkStatus()">...</div>

<!-- Fast interval (500ms) -->
<div x-interval.500ms="$action.get('/api/live')">...</div>

Modifiers

<!-- Only run when tab is visible -->
<div x-interval.visible.5s="$action.get('/api/status')">...</div>

<!-- CSRF-protected requests -->
<div x-interval.2s="$action('/api/protected')">...</div>

<!-- Stop on condition -->
<div x-data="{ done: false, progress: 0 }"
     x-interval.1s="progress += 10; done = progress >= 100"
     x-interval-stop="done">
    Processing...
</div>
Modifier Description
.{time} Interval duration (e.g., .5s, .500ms)
.visible Only run when tab visible

Note: For state-mutating requests, use $action() which automatically includes CSRF protection.

Confirmation Dialogs

x-confirm Directive

<!-- Default message -->
<button @click="$action.delete('/item/1')" x-confirm>Delete</button>

<!-- Custom message -->
<button
    @click="$action.delete('/item/1')"
    x-confirm="Are you sure you want to delete this item?"
>
    Delete
</button>

<!-- Dynamic message -->
<button
    @click="$action.delete('/user/' + userId)"
    x-confirm="'Delete ' + userName + '?'"
>
    Delete
</button>

Configuration

Alpine.gale.configureConfirm({
    defaultMessage: "Are you sure?",
    handler: async (message) => {
        // Custom modal
        return await myModal.confirm(message);
    },
});

📖 See the full Frontend Reference for all directives and magics.

Advanced Topics

SSE Protocol Specification

Gale uses Server-Sent Events with specific event types and data formats.

Event Types

Event Purpose
gale-patch-state Merge state into Alpine component
gale-patch-elements DOM manipulation
gale-patch-component Update named component
gale-invoke-method Call method on component

gale-patch-state Format

event: gale-patch-state
data: state {"count":1,"user":{"name":"John"}}
data: onlyIfMissing false
Data Line Description
state {json} State to merge
onlyIfMissing {bool} Only set if missing

gale-patch-elements Format

event: gale-patch-elements
data: selector #content
data: mode outer
data: elements <div id="content">...</div>
data: useViewTransition true
data: settle 100
data: limit 10
data: scroll bottom
data: show top
data: focusScroll true
Data Line Description
selector {css} Target element(s)
mode {mode} Patch mode (default: outer)
elements {html} HTML content (can span multiple lines)
useViewTransition {bool} Use View Transitions
settle {ms} Delay before patch
limit {n} Max elements
scroll {top|bottom} Auto-scroll target after swap
show {top|bottom} Scroll element into viewport
focusScroll {bool} Maintain focus scroll position

gale-patch-component Format

event: gale-patch-component
data: component cart
data: state {"total":42}
data: onlyIfMissing false

gale-invoke-method Format

event: gale-invoke-method
data: component cart
data: method recalculate
data: args [10,20]

State Serialization

When making requests, Alpine Gale serializes the component's x-data based on the x-sync directive.

What Gets Serialized

  • Properties declared in x-sync directive (or all properties if x-sync is empty/wildcard)
  • If no x-sync: only properties specified in include option
  • Form fields with name attribute (unless includeFormFields: false)
  • Nested objects (recursively)
  • Arrays

What Doesn't Get Serialized

  • Properties not declared in x-sync (unless in include option)
  • Functions
  • DOM elements
  • Circular references (skipped)
  • Properties starting with _ or $

Controlling Serialization

Use the x-sync directive for component-level control:

<!-- Recommended: Declare synced properties at component level -->
<div x-data="{ user: {...}, temp: null }" x-sync="['user']">
  <button @click="$action('/save')">Save User</button>
</div>

Use request options for per-request overrides:

<button
    @click="$action('/save', {
    include: ['additionalKey'],
    exclude: ['sensitiveKey']
})"
>
    Save
</button>

Component Inclusion

<button
    @click="$action('/save', {
    includeComponents: ['cart', 'wishlist'],
    includeComponentsByTag: ['forms']
})"
>
    Save All
</button>

DOM Patching Modes

Gale provides 9 DOM patching modes organized into three categories:

State Handling Categories

Category Modes How State Works
Server-driven outer (DEFAULT), inner State comes from x-data in response HTML via Alpine.initTree()
Client-preserved outerMorph, innerMorph Existing Alpine state preserved via Alpine.morph() smart diffing
Insertion/Deletion before, after, prepend, append, remove New elements initialized, existing elements unchanged

Complete Mode Reference

Mode Aliases Description Alpine Method
outer (DEFAULT) outerHTML, replace Replace entire element with server-driven state initTree()
inner innerHTML Replace inner content with server-driven state initTree()
outerMorph morph Smart morph entire element, client-side state preserved Alpine.morph()
innerMorph morph_inner Smart morph children, wrapper state preserved Alpine.morph()
before beforebegin Insert before target element initTree()
after afterend Insert after target element initTree()
prepend afterbegin Insert at start inside target initTree()
append beforeend Insert at end inside target initTree()
remove delete Delete element with cleanup

Choosing Between outer and outerMorph

Use outer (default) when:

  • The server controls all state (e.g., server-rendered forms)
  • You want a clean state reset from the server
  • Performance is critical (replacement is faster than morphing)

Use outerMorph when:

  • Client has local state that must survive the update (e.g., counters, toggles)
  • Form input focus/values should be preserved
  • Smooth transitions without flicker are needed

Example: State Behavior Difference

// Server response includes x-data="{ count: 99 }"

// outer: Client count=2 → Server sends count=99 → Result: count=99
gale()->outer('#counter', $html);

// outerMorph: Client count=2 → Server sends count=99 → Result: count=2 (preserved!)
gale()->outerMorph('#counter', $html);

Viewport Modifiers

Control scrolling behavior after DOM patches:

Option Values Description
scroll 'top', 'bottom' Auto-scroll target element internally
show 'top', 'bottom' Scroll element into viewport
focusScroll true, false Maintain focus scroll position
// Chat: Append message and scroll to bottom
gale()->append('#chat-messages', $messageHtml, ['scroll' => 'bottom']);

// Form: Update and scroll into view
gale()->outer('#form', $html, ['show' => 'top']);

// Preserve focus position during morph
gale()->outerMorph('#editor', $html, ['focusScroll' => true]);

Backward Compatibility

These Gale v1 names continue to work:

Old Name Maps To Notes
morph outerMorph State-preserving morph
morph_inner innerMorph Children morph
replace outer Full replacement

HTMX Compatibility Aliases

For developers familiar with HTMX:

HTMX Name Maps To
outerHTML outer
innerHTML inner
delete remove
beforebegin before
afterend after
afterbegin prepend
beforeend append

View Transitions API

Enable smooth page transitions:

gale()->view('page', $data, ['useViewTransition' => true]);

gale()->html($html, [
    'selector' => '#content',
    'useViewTransition' => true,
]);

CSS:

::view-transition-old(root) {
    animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
    animation: fade-in 0.3s ease-in;
}

@keyframes fade-out {
    to {
        opacity: 0;
    }
}
@keyframes fade-in {
    from {
        opacity: 0;
    }
}

Falls back gracefully in unsupported browsers.

Conditional Execution

when() / unless()

gale()->when($condition, function ($gale) {
    $gale->state('visible', true);
});

gale()->when(
    $user->isAdmin(),
    fn($g) => $g->state('role', 'admin'),
    fn($g) => $g->state('role', 'user')
);

gale()->unless($user->isGuest(), function ($gale) use ($user) {
    $gale->state('user', $user->toArray());
});

whenGale() / whenNotGale()

gale()->whenGale(
    fn($g) => $g->state('partial', true),
    fn($g) => $g->web(view('full'))
);

gale()->whenNotGale(function ($gale) {
    return view('full-page');
});

whenGaleNavigate()

gale()->whenGaleNavigate('sidebar', function ($gale) {
    $gale->fragment('layout', 'sidebar', $data);
});

web()

Set response for non-Gale requests:

return gale()
    ->state('data', $data)
    ->web(view('page', compact('data')));

📖 Explore Advanced Topics for SSE protocols, streaming, and more.

API Reference

GaleResponse Methods

Method Signature Description
state state(string|array $key, mixed $value = null, array $options = []) Set state
forget forget(string|array $keys) Remove state keys
messages messages(array $messages) Set messages state
clearMessages clearMessages() Clear messages
view view(string $view, array $data = [], array $options = [], bool $web = false) Render view
fragment fragment(string $view, string $fragment, array $data = [], array $options = []) Render fragment
fragments fragments(array $fragments) Render multiple fragments
html html(string $html, array $options = [], bool $web = false) Patch HTML
outer outer(string $selector, string $html, array $options = []) Replace element (server state)
inner inner(string $selector, string $html, array $options = []) Replace inner (server state)
outerMorph outerMorph(string $selector, string $html, array $options = []) Morph element (preserve state)
innerMorph innerMorph(string $selector, string $html, array $options = []) Morph children (preserve state)
append append(string $selector, string $html, array $options = []) Append HTML
prepend prepend(string $selector, string $html, array $options = []) Prepend HTML
before before(string $selector, string $html, array $options = []) Insert before
after after(string $selector, string $html, array $options = []) Insert after
remove remove(string $selector) Remove element
morph morph(string $selector, string $html, array $options = []) Alias for outerMorph
replace replace(string $selector, string $html, array $options = []) Alias for outer
delete delete(string $selector) Alias for remove
js js(string $script, array $options = []) Execute JavaScript
dispatch dispatch(string $event, array $data = [], array $options = []) Dispatch event
navigate navigate(string|array $url, string $key = 'true', array $options = []) Navigate
navigateWith navigateWith(string|array $url, string $key = 'true', bool $merge = false, array $options = []) Navigate with merge control
navigateMerge navigateMerge(string|array $url, string $key = 'true', array $options = []) Navigate with merge
navigateClean navigateClean(string|array $url, string $key = 'true', array $options = []) Navigate without merge
navigateOnly navigateOnly(string|array $url, array $only, string $key = 'true') Keep only params
navigateExcept navigateExcept(string|array $url, array $except, string $key = 'true') Remove params
navigateReplace navigateReplace(string|array $url, string $key = 'true', array $options = []) Replace history
updateQueries updateQueries(array $queries, string $key = 'filters', bool $merge = true) Update query params
clearQueries clearQueries(array $paramNames, string $key = 'clear') Clear query params
reload reload() Reload page
componentState componentState(string $name, array $state, array $options = []) Update component
componentMethod componentMethod(string $name, string $method, array $args = []) Call component method
redirect redirect(string $url) Create redirect
stream stream(callable $callback) Stream mode
when when(mixed $condition, callable $true, ?callable $false = null) Conditional
unless unless(mixed $condition, callable $callback) Inverse conditional
whenGale whenGale(callable $gale, ?callable $web = null) Gale request check
whenNotGale whenNotGale(callable $callback) Non-Gale check
whenGaleNavigate whenGaleNavigate(?string $key, callable $callback) Navigate check
web web(mixed $response) Set web fallback
withEventId withEventId(string $id) Set SSE event ID
withRetry withRetry(int $ms) Set SSE retry
reset reset() Clear events

GaleRedirect Methods

Method Signature Description
with with(string|array $key, mixed $value = null) Flash data
withInput withInput(?array $input = null) Flash input
withErrors withErrors(mixed $errors) Flash errors
back back(string $fallback = '/') Go back
backOr backOr(string $route, array $params = []) Back with route fallback
refresh refresh(bool $query = true, bool $fragment = false) Refresh page
home home() Go to root
route route(string $name, array $params = [], bool $absolute = true) Named route
intended intended(string $default = '/') Auth intended
forceReload forceReload(bool $bypass = false) Hard reload

Request Macros Reference

Macro Signature Description
isGale isGale() Check Gale request
state state(?string $key = null, mixed $default = null) Get state
isGaleNavigate isGaleNavigate(string|array|null $key = null) Check navigate
galeNavigateKey galeNavigateKey() Get navigate key
galeNavigateKeys galeNavigateKeys() Get navigate keys array
validateState validateState(array $rules, array $messages = [], array $attrs = []) Validate state

Frontend Magics Reference

Magic Description
$action(url, options?) POST with auto CSRF (default)
$action.get(url, options?) GET request
$action.post(url, options?) POST with auto CSRF
$action.put(url, options?) PUT with auto CSRF
$action.patch(url, options?) PATCH with auto CSRF
$action.delete(url, options?) DELETE with auto CSRF
$gale Global connection state
$fetching() Element loading state (function)
$navigate(url, options?) Programmatic navigation
$components Component registry API
$invoke(name, method, ...args) Shorthand for $components.invoke
$file(name) Get file info
$files(name) Get files array
$filePreview(name, index?) Get preview URL
$clearFiles(name?) Clear files
$formatBytes(size, decimals?) Format bytes
$uploading Upload in progress
$uploadProgress Upload progress 0-100
$uploadError Upload error message

Frontend Directives Reference

Directive Description
x-sync Sync all state to server (wildcard)
x-sync="['a','b']" Sync specific state keys
x-navigate Enable SPA navigation
x-navigate-skip Skip navigation handling
x-component="name" Register named component
x-name="field" Form binding with state
x-files File input binding
x-message="key" Display message
x-loading Loading state display
x-indicator="var" Create loading variable
x-poll="url" Auto-polling
x-poll-stop="expr" Stop polling condition
x-confirm Confirmation dialog

Request Options Reference

Option Type Default Description
include string[] Add keys to x-sync (union)
exclude string[] Remove keys from result
includeFormFields boolean true Include form field values
headers object {} Additional headers
retryInterval number 1000 Initial retry (ms)
retryScaler number 2 Backoff multiplier
retryMaxWaitMs number 30000 Max retry wait (ms)
retryMaxCount number 10 Max retries
requestCancellation boolean false Cancel previous
onProgress function Progress callback
includeComponents string[] Include component states
includeComponentsByTag string[] Include by tag

SSE Events Reference

Event Data Lines
gale-patch-state state, onlyIfMissing
gale-patch-elements selector, mode, elements, useViewTransition, settle, limit, scroll, show, focusScroll
gale-patch-component component, state, onlyIfMissing
gale-invoke-method component, method, args

Configuration Reference

Backend (config/gale.php)

return [
    'route_discovery' => [
        'enabled' => false,
        'discover_controllers_in_directory' => [],
        'discover_views_in_directory' => [],
        'pending_route_transformers' => [
            ...Dancycodes\Gale\Routing\Config::defaultRouteTransformers(),
        ],
    ],
];

Frontend (JavaScript)

// CSRF
Alpine.gale.configureCsrf({
    headerName: "X-CSRF-TOKEN",
    metaName: "csrf-token",
    cookieName: "XSRF-TOKEN",
});

// Messages
Alpine.gale.configureMessage({
    defaultStateKey: "messages",
    autoHide: true,
    autoShow: true,
    typeClasses: {
        /* ... */
    },
});

// Confirmation
Alpine.gale.configureConfirm({
    defaultMessage: "Are you sure?",
    handler: (message) => confirm(message),
});

// Navigation
Alpine.gale.configureNavigation({
    // Navigation options
});

// Get current configs
Alpine.gale.getCsrfConfig();
Alpine.gale.getMessageConfig();
Alpine.gale.getConfirmConfig();
Alpine.gale.getNavigationConfig();

Troubleshooting

Multiple Alpine Instances

If you see this error in the console:

Detected multiple instances of Alpine running

Or Alpine magics like $wire or $action are undefined, you have two versions of Alpine running. Gale bundles Alpine.js, so you must remove any other Alpine installation:

Remove CDN script:

<!-- Remove this line -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>

Remove npm import (Laravel Breeze, etc.):

// Remove from resources/js/app.js:
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Then use @gale — it provides Alpine.js, Morph, and Gale together.

Common Issues

Issue Cause Solution
"Multiple instances" Duplicate Alpine.js Remove existing Alpine (see above)
"No Alpine context" Magic used outside x-data Wrap in x-data element
CSRF token mismatch Token not sent Use $action() (auto CSRF)
State not updating Wrong key Check Alpine x-data property names
419 error Session expired Refresh page or use meta token
Navigation not working Missing x-navigate Add directive to link/form
File upload fails Missing x-files Add directive to file input
Messages not showing Wrong key Check x-message matches server key
Polling not stopping Condition never true Check x-poll-stop expression

Debugging

<!-- Log all state changes -->
<div x-data="{ count: 0 }" x-init="$watch('count', v => console.log(v))">
    <!-- Check $gale state -->
    <div x-data x-init="console.log($gale)">
        <!-- View component registry -->
        <button @click="console.log($components.all())">Debug</button>
    </div>
</div>

Testing

Package Tests

cd packages/dancycodes/gale
vendor/bin/phpunit
vendor/bin/phpstan analyse
vendor/bin/pint

License

MIT License. See LICENSE.

Credits

Created by DancyCodesdancycodes@gmail.com

Resources

统计信息

  • 总下载量: 22
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-11-23