vortexphp / live
Server-driven Live components (Livewire-style) for Vortex PHP
Requires
- php: ^8.2
- vortexphp/framework: ^0.11
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is not auto-updated.
Last update: 2026-04-05 06:20:22 UTC
README
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.
Installation
- Require the package; allowlist component FQCNs in app config (
live.components). - Register the Twig extension (
live_mount) in your app. - Expose the client script (copy
resources/live.jsto your web root or run your project’s sync step aftercomposer install).
Config (example)
// config/live.php return [ 'components' => [ App\Live\Components\Counter::class, ], ];
Layout
<script src="/js/live.js" defer></script>
Twig extension (your app wiring may differ)
$twig->addExtension(new \Vortex\Live\Twig\LiveExtension());
Component island (server-rendered root)
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.
Actions (server)
| 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>
Model binding modes
| 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>
Validation
| 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>
Local mirrors
| 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>
Conditional visibility (live:show / live:hide)
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>
Scope templates (JSON in DOM)
| 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>
Arrays / lists in local state
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 %}
Internal attributes (do not hand-author)
The runtime may set data-live-model-bound, data-live-template-id, and data-live-from-template on nodes it manages.
Source file
- Client:
resources/live.js(single IIFE, no bundler).