Server-driven Live components (Livewire-style POST + signed snapshot) with a small vanilla client runtime (resources/live.js). Public markup uses the live:* attribute namespace documented below.
For product direction and parity notes, see ROADMAP.md.
- Require
vortexphp/framework^0.12 and this package. - Allowlist component FQCNs in app config (
live.components). - Register
Vortex\Live\LivePackageinconfig/app.phpunderpackages(it adds thelive_mountTwig extension andPOST /live/message). - Publish the client script:
php vortex publish:assets(declared byLivePackage::publicAssets()→public/js/live.js). Run aftercomposer install/update, or add a Composer script that calls it.
Config (example)
// config/app.php (merge)
'packages' => [
\Vortex\Live\LivePackage::class,
],
// config/live.php
return [
'components' => [
\Vortex\vortex\app\Components\Live\Counter::class,
],
];Layout
<script src="/js/live.js" defer></script>(publish:assets places resources/live.js there.)
If you do not use application packages, register Vortex\Live\Twig\LiveExtension via app.twig_extensions, wire POST /live/message to LiveController::message, and copy resources/live.js into public/js/ yourself.
live_mount('App\\Live\\Components\\MyComponent', props) wraps the view in a root element with:
| Attribute | Purpose |
|---|---|
live-root |
Marks the island boundary. |
live-state |
Signed snapshot token (HMAC). |
live-url |
POST endpoint for actions / sync (e.g. /live/message). |
live-csrf |
CSRF token for POST JSON body. |
Twig
{{ live_mount('App\\Live\\Components\\Counter', { count: 0 }) }}Rough HTML shape (attributes are emitted by PHP; don’t paste live-state by hand)
<div class="live-root" live-root live-state="…" live-url="/live/message" live-csrf="…">
{# your component twig #}
</div>All live:click, live:submit, and live:model.live / live:model.lazy behavior applies inside this subtree.
| Syntax | Meaning |
|---|---|
live:click="methodName" |
On click, POST action: methodName with args from live:args. Element must be inside [live-root]. |
live:submit="methodName" |
On form submit, POST that action; merge includes bound fields + FormData. |
live:args='[1,"a"]' |
Optional JSON array only. Invalid JSON or non-array → action is not sent. Single argument: live:args='[0]'. |
Methods are invoked on the PHP component with ReflectionMethod::invokeArgs — arity must match.
Button + args
<button type="button" live:click="increment">+1</button>
<button type="button" live:click="add" live:args="[5]">+5</button>
<button type="button" live:click="pickRow" live:args='[0]'>First row</button>Twig loop
{% for item in items %}
<button type="button" live:click="remove" live:args='[{{ item.id }}]'>Remove</button>
{% endfor %}Form
<form live:submit="save">
<input name="title" />
<button type="submit">Save</button>
</form>| Attribute | Behavior |
|---|---|
live:model.local="prop" |
Client-only until the next server round-trip; value is merged from the DOM when an action/submit/sync runs. |
live:model.live="prop" |
Debounced POST with sync: true (re-render, no named action). |
live:model.lazy="prop" |
Sync on change / commit-style events. |
Supported controls: input (text, checkbox, radio, number, …), textarea, select.
Examples
<input type="text" live:model.live="title" value="{{ title }}" />
<textarea live:model.lazy="body">{{ body }}</textarea>
<input type="checkbox" live:model.local="agree" />
<select live:model.local="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>| Syntax | Meaning |
|---|---|
live-error="fieldName" |
Node whose textContent is filled when the server returns validation_failed + errors[fieldName]. |
Example
<form live:submit="save" novalidate>
<textarea live:model.lazy="note" name="note"></textarea>
<p live-error="note"></p>
<button type="submit">Save</button>
</form>| Syntax | Meaning |
|---|---|
live-display="prop" |
Text node mirroring the formatted value of live:model.local="prop" on the same island (updated as the user types). |
Example
<input type="text" live:model.local="scratch" value="" />
<span live-display="scratch"></span>Inside a live-root island, toggle hidden from the current value of any bound property (live:model.local | .live | .lazy with the same name).
| Syntax | Meaning |
|---|---|
live:show="prop" |
Visible when prop is truthy (non-empty string, non-zero number, true, checked checkbox, etc.). |
live:hide="prop" |
Hidden when prop is truthy. |
Use one of the two per element (not both on the same node). Updates run when the bound control changes and once after bindings init.
Falsy: null, undefined, false, '' / whitespace-only string, 0.
Example
<input type="checkbox" live:model.local="agree" />
<div live:show="agree">Thanks for agreeing.</div>
<div live:hide="agree">Please check the box.</div>| Attribute | Meaning |
|---|---|
live:scope='{"path":{"nested":true}}' |
JSON object on a container. |
<template live:for-each="dot.path"> |
For each object key or array index at path, clone template content as siblings. |
live:slot="key" |
value |
Runs on load and after each Live HTML swap on the new root. Add live:model.local inside cloned nodes if you need bindings.
Example
<ul live:scope='{"car":{"make":"Jeep","model":"Wrangler"}}'>
<template live:for-each="car">
<li><span live:slot="key"></span>: <span live:slot="value"></span></li>
</template>
</ul>There is no single live:model for a JSON array. Use one of:
- Flat props —
item0_title,item1_title, … plus Twig{% for %}andlive:model.local="item{{ i }}_title". live:scopeJSON — client-side templated list; good for display/expansion; binding to snapshot still uses flat props if you merge to the server.
Twig: fixed slots
{% for i in 0..2 %}
<input type="text" live:model.local="row{{ i }}_label" value="{{ attribute(_context, 'row' ~ i ~ '_label') }}" />
{% endfor %}The runtime may set data-live-model-bound, data-live-template-id, and data-live-from-template on nodes it manages.
- Client:
resources/live.js(single IIFE, no bundler).