$server
$server gives Alpine expressions and JavaScript access to an island's state and actions. It resolves to the nearest containing island.
Use it inside native Alpine directives: @click="$server.call('save')" sends an action, while x-show="$server.loading('save')" only observes request feedback. Alpine owns the event handling and presentation. For local state, bindings, and transitions, see Alpine and Interact.
| Member | Description |
|---|---|
state | Props and state, including local edits |
call(action, ...args) | Call a declared PHP action |
sync() | Send edits and render without an action |
pending(target?, ...args) | Match queued or executing requests |
loading(target?, ...args) | Match requests after a visual delay |
busy | Whether a request is executing |
dirty | Whether edits or file selections need synchronization |
errors | Field validation messages |
error | Current request failure or null |
paused | Whether automatic updates are paused after a failure |
exportDraft() | Export writable values as JSON |
receiveStream(message) | Display a normalized message from a custom adapter |
uploads | Per-field file progress and controls |
state
<p x-text="'Count: ' + $server.state.count"></p>For #[State] public int $count, PHP sees $count and JavaScript sees $server.state.count. Props are available the same way. Only writable state can receive browser edits. Entities and viewData() are not included.
Use x-interact:model for inputs. Local edits appear in state immediately; PHP-rendered text changes on the next rendering response.
call()
<button type="button" @click="$server.call('adjust', 5)">Add five</button>call(action, ...args): Promise<RequestResult> queues a declared #[Action] method with JSON-compatible arguments. PHP validates types and application rules. See Actions for the corresponding adjust() implementation. Renderless actions use this same method and result contract; their accepted state updates do not morph the view.
PHP actions can request redirects and downloads. No additional bridge methods are needed. A successful download action result means the download was requested, not that its transfer completed.
sync()
<button type="button" @click="$server.sync()" :disabled="$server.busy">
Refresh
</button>sync(): Promise<RequestResult> sends current writable edits and renders the island without invoking an action. Fresh viewData() can query new data. It does not rerun the controller or mount(), or automatically save a record. Declared URL bindings publish accepted values automatically.
Actions queue in order; pending synchronization coalesces. Each island processes one request at a time. Events can trigger another island's refresh; x-interact:poll schedules periodic refreshes.
pending()
<button
type="button"
@click="$server.call('save')"
:disabled="$server.pending('save')"
>
Save
</button>pending() becomes true immediately for matching accepted queued or executing work. Ordinary action buttons remain repeatable unless explicitly disabled; x-interact:submit protects forms automatically.
| Query | Matches |
|---|---|
pending() | All accepted requests, including transfers |
pending('save') | Any invocation of save |
pending('remove', 42) | remove with the complete argument tuple [42] |
pending(null) | Synchronization requests |
pending({ action: 'remove', args: [42] }) | Equivalent object selector |
Omitting action matches all requests; null matches synchronization. Argument filters require a named action and its complete argument tuple. Scalar types are preserved, so 42 and '42' differ. Object key order is ignored. Unsent model debounce timers do not count.
loading()
<span role="status" x-show="$server.loading('save')" x-cloak>Saving…</span>loading() accepts the same targets as pending(), with the configured loading delay, initially 200 ms. The object form can set a delay from 0 to 60,000 whole milliseconds:
<span
role="status"
x-show="$server.loading({ action: 'remove', args: [42], delay: 500 })"
x-cloak
>Removing…</span
>The delay controls presentation only. It includes continuously matching queued and executing work; overlapping matching requests keep the indicator visible. Equivalent queries share timers. Resources are released when matching work ends or the island disappears. Add [x-cloak] { display: none !important; } to the stylesheet when using x-cloak.
busy
busy is true while a request executes. It excludes queued work and drives the island root's aria-busy. Form submission protection also covers queueing time.
dirty
dirty is true for unsynchronized edits, unfinished transfers, and failed file selections. It does not describe whether database changes were saved. Accepted state and staged files can still be unsaved application data.
errors
errors maps fields to message lists. PHP supplies these through setErrors(). A renderable HTTP 422 updates field feedback without pausing the island. FormHelper can read the entity's original validation errors separately.
Errors supplied during mount() are available immediately, including on the first eager render. Deferred islands receive their initial feedback when loading finishes. A subsequent successful request replaces that feedback normally.
<ul role="alert">
<template
x-for="(message, index) in ($server.errors.title || [])"
:key="index"
>
<li x-text="message"></li>
</template>
</ul>error
<p role="alert" x-text="$server.error ? $server.error.message : ''"></p>error is null or { code, message, status, recovery }. See Error handling for failure categories and recovery. Ordinary handlers do not need .catch() markup.
paused
paused is true after a request failure pauses automatic updates. Local editing continues. Explicit recovery depends on the failure's recovery value; failed actions are never automatically retried.
exportDraft()
exportDraft(): string returns formatted JSON for all current writable values, including synchronized values. It excludes props, server-owned state, files and upload references, snapshots, and runtime metadata. It does not persist data.
For a user-controlled copy field:
<label for="draft-export">Copy draft</label>
<textarea id="draft-export" readonly :value="$server.exportDraft()"></textarea><?= $this->Form->control('draftExport', [
'type' => 'textarea',
'label' => 'Copy draft',
'readonly' => true,
':value' => '$server.exportDraft()',
]) ?>Writable data may be sensitive. Offer copying intentionally; see recovery.
uploads
For a declared upload property named document:
<progress
max="100"
aria-label="Upload progress"
x-show="$server.uploads.document.pending"
:value="$server.uploads.document.progress"
></progress>
<button type="button" @click="$server.uploads.document.cancel()">
Cancel transfer
</button>Each field exposes pending, progress, files, cancel(id?), remove(id?), and retry(id?). Progress is 0–100 or null. File items expose id, name, size, status, and progress. Status is queued, uploading, ready, or failed. Omit an ID to target the field's applicable items.
Cancellation does not guarantee PHP stopped processing. Retry transfers files; it never replays a failed save. Accepted files are PHP uploaded-file objects. See File uploads for staging behavior and limits.
Request results
call() and sync() return { ok: true } after an accepted response (render or navigation), or { ok: false, error } after a request failure. An accepted HTTP 422 can contain validation errors, so ok does not prove a save succeeded.
Inside a registered Alpine method:
async save() {
const result = await this.$server.call('save');
if (!result.ok) console.log(result.error.code);
}Use this.$server in JavaScript methods and $server in template expressions. See Alpine registration. Removed islands settle waiting requests with cancelled and do not emit new error UI. Aborting fetch does not cancel server execution.
Before a deferred mount
For a pending island, sync() initializes its content. Request feedback works normally; state and uploads remain empty until loading succeeds. Actions, model bindings, and polling require loaded content. Repeated initialization triggers share one request.
receiveStream()
receiveStream(message: unknown): void validates and displays a normalized stream message from an application-owned transport adapter. It does not start a connection or request.
server.receiveStream({
version: 1,
id: islandId,
type: 'target',
to: 'answer',
content: 'Next result',
replace: false,
html: false,
});id must match the receiving island's data-interact-id. Target messages require to, content, replace, and html; event messages require type: 'event', name, and JSON-compatible record detail. Both include version: 1 and id. The npm package exports StreamMessage for these shapes.
Targets obey x-interact:stream ownership and mode checks. Unknown or invalid messages throw. Calls after disposal are ignored. This method accepts provisional display messages; normal requests exclusively own signed state and terminal results. Applications own subscription authorization and cleanup.
realtime
$server.realtime exposes managed subscription feedback without provider credentials:
| Member | Meaning |
|---|---|
status | disabled, connecting, connected, reconnecting, or error |
stale | A matching change is waiting for an authoritative render |
error | Safe connection/subscription feedback, or null |
Connection failures do not pause ordinary island actions. Automatic refresh requests retain normal request feedback and failure handling. See realtime subscriptions.