LiveDomJS
zero build step · zero routes · zero JS files

HTML gets a nervous system.
Laravel stays exactly where it is.

LiveDomJS turns HTML attributes into a full reactivity engine for Laravel Blade — AJAX, spreadsheet-grade live computation, WebSocket broadcasting, and SPA navigation — without writing a single line of client-side JavaScript.

<!-- this is the entire feature -->
<button live-click="saveUser" live-realtime="true">
  Save
</button>
30+
live-* attributes
0
new routes/files
6
aggregate functions
1
composer command
01 — Overview

What is LiveDomJS?

LiveDomJS is a lightweight JavaScript engine that lets Blade templates call existing Laravel controllers directly from HTML attributes. There is no separate API layer to design, no JavaScript state management to write, and no new routes to register — every interaction is described declaratively on the element that triggers it.

Under the hood, every attribute you add is handled by one of four engines that ship inside livedom.js: the AJAX/event engine (click, change, submit, hover, keyup, polling), the Live Compute engine (client-side spreadsheet-style formulas), the real-time engine (WebSocket broadcasting through Laravel Reverb), and the SPA engine (region-based navigation with history support).

// convention

live-click="saveUser" sends a request to /ajax/{live-scope}/saveUser, which Laravel's AjaxController dispatches straight to App\Http\Controllers\{live-scope}@saveUser. The controller never knows it was called this way.

// no jQuery in your code

LiveDomJS still bundles jQuery internally today (see roadmap), but you never write jQuery yourself — every selector, event binding, and DOM patch is generated from the attributes you declare.

02 — Getting started

Installation

Requirements: Laravel 9, 10, 11, or 12 · PHP 8.0+

# that's the whole install
composer require gadingrengga/livedomjs

Nothing else is required. The moment the package is required by Composer:

  • The route /ajax/{controller}/{action} is registered automatically by LiveDomServiceProvider.
  • The CSRF meta tag is injected automatically whenever real-time assets are active — you don't need to add it by hand.
  • Assets are served directly from the package via a fallback route — no vendor:publish needed to get started.

Optional: publishing & customizing

If you want to change the route prefix, disable auto-injection, or edit livedom.js directly in your project:

php artisan livedomjs:install --publish

This publishes config/livedomjs.php and the JS assets to public/vendor/livedomjs. Once published, your local copy automatically takes priority over the package's built-in fallback.

⚠ Laravel 11 & 12 — one extra step for real-time only

New Laravel 11/12 projects ship a "slim skeleton" without config/broadcasting.php or routes/channels.php. AJAX, live-compute, and SPA work with zero setup on any version — only live-realtime needs broadcasting configured first:

php artisan install:broadcasting

On Laravel 9/10, config/broadcasting.php already exists, so you only need:

composer require laravel/reverb
php artisan reverb:install

Once REVERB_APP_KEY is set in .env, LiveDomJS detects it automatically and starts injecting the realtime script stack. Until then, live-realtime is silently inactive — no errors, no "Echo is not defined". Keep a broadcast server and queue worker running: php artisan reverb:start and php artisan queue:work.

Your first reactive element

<div live-scope="UserController">
  <input name="username" placeholder="Enter username...">
  <button live-click="checkAvailability" live-target="#status">Check</button>
  <div id="status"></div>
</div>
// app/Http/Controllers/UserController.php — no new routes or files needed
public function checkAvailability(Request $request)
{
    $exists = User::where('username', $request->username)->exists();

    return $exists
        ? '<span class="text-red-500">Username taken</span>'
        : '<span class="text-green-500">Username available</span>';
}

AjaxController wraps whatever you return into the { success, message, data } envelope automatically — your method just returns the raw payload (a string, an array, or a Blade View, which gets ->render()'d to a string for you). Don't wrap it yourself in response()->json([...])AjaxController only unwraps View instances specially, so a hand-built JsonResponse ends up nested as-is under data instead of the string/array you meant to send. On failure, throw or let the exception propagate — LiveDomJS shows an error modal (dev) or toast (production) for you automatically.

03 — Core concepts

How a request actually flows

User clicks [live-click="saveUser"]
    │
    ▼
POST /ajax/UserController/saveUser        ← route registered by LiveDomServiceProvider
    │
    ▼
AjaxController::handle()                  ← resolves "UserController" → App\Http\Controllers\UserController
    │
    ▼
UserController@saveUser($request)         ← your existing controller, untouched
    │
    ▼
{ success: true, data: "<html or json>" }
    │
    ▼
Rendered into [live-target="#result"]     ← or passed to a callback / autoBindDomFromResponse()

Controller names may include / or . to reach nested namespaces — live-scope="Invoice/ItemController" resolves to App\Http\Controllers\Invoice\ItemController. Both controller and action names are validated with a strict whitelist regex before resolution, so arbitrary class names can never be dispatched.

live-scope — the request boundary

Every element with a live-* trigger needs a live-scope ancestor. It does two jobs at once: it tells LiveDomJS which controller to call, and it defines which inputs on the page are allowed to travel with the request. Only <input>/<select>/<textarea> elements inside the nearest live-scope ancestor are serialized — nothing outside that boundary leaks in. This is what makes repeating rows (tables, line items) safe by default.

<!-- Row 1 — only its own inputs are sent -->
<tr live-scope="Order/ItemController">
  <td><input name="qty"></td>
  <td><button live-click="update">Update</button></td>
</tr>
<!-- Row 2 — completely isolated, even though it's the same controller -->
<tr live-scope="Order/ItemController">
  <td><input name="qty"></td>
  <td><button live-click="update">Update</button></td>
</tr>

live-dom — how the response lands

By default a successful response replaces the target's innerHTML (or its value, if the target is a form field). live-dom lets you pick a different DOM action explicitly:

ValueEffect
autoDefault. value for form fields, innerHTML otherwise.
htmlReplace innerHTML.
textReplace textContent (no HTML parsing).
value / valSet an input's value and dispatch change/input.
append / prependInsert HTML at the start/end of the target.
before / afterInsert HTML as a sibling of the target.
show / hide / toggleVisibility only — ignores the response body (toggle uses response as truthy flag).
removeRemoves the target element(s) entirely.

You can combine several actions with several targets in one call: live-target="#a, #b" paired with live-dom="append, hide" applies each action to its matching target by index.

04 — Interaction

Trigger attributes

Each of these binds a DOM event to one or more controller methods. All of them are delegated at the document level, so they work on content that's injected later (AJAX responses, SPA navigation) without any re-initialization step on your part.

AttributeFires onNotes
live-clickclickMost common trigger.
live-changechangeSelects, checkboxes, blur-commit fields.
live-inputinput (debounced)400ms debounce per controller+method, shared with live-keyup.
live-keyupkeyup (debounced)Good for search-as-you-type.
live-submitform submitpreventDefault() is automatic.
live-hovermouseenterDelegated hover (mouseover/mouseout + relatedTarget check), so it works on dynamic content too.
live-pollinterval, in msRepeats a GET call to whatever method is set on live-click on the same element.
<button live-click="store">Create</button>
<input live-change="search">
<form live-submit="processForm">
  <div live-hover="preview"></div>
</form>
<div id="ticker" live-poll="5000" live-click="refresh"></div>

live-method overrides the HTTP verb used for the request (defaults to POST, or the enclosing <form method> for submits): live-method="GET".

05 — Interaction

Sending data & calling methods with arguments

Default: everything in scope

With no extra configuration, LiveDomJS collects every named input inside the nearest live-scope and sends it as FormData (files, checkboxes, and radios are handled the same way a native form submit would).

Multiple methods, one element

Comma-separate method calls inside a single live-click — each can carry its own arguments and, paired with a comma-separated live-target, its own target:

<button
  live-click="logAction('viewed'), refreshPanel"
  live-target="#log, #panel"
>View</button>

Passing explicit arguments

Call the method like a JS function directly in the attribute. Arguments are evaluated safely (not eval'd against global scope) and sent as data in the request body — single argument, array of arguments, selector string, or a plain object literal are all supported:

<!-- single scalar argument -->
<button live-click="deleteItem(42)">Delete</button>

<!-- object literal -->
<button live-click="updateStatus({id: 42, status: 'done'})">Complete</button>

<!-- 'this' refers to the triggering element -->
<button live-click="remove(this.dataset.id)" data-id="42">Remove</button>

<!-- a #id/.class argument scopes data collection to THAT element instead -->
<button live-click="updateDimension('#tr-1')">Update row</button>

Your controller receives whichever shape you sent under $request->input('data') when args are used, or the raw named fields when the scope is serialized normally.

live-data — static payload override

When you need to send a fixed value regardless of scope contents, live-data takes priority over both scope serialization and inline arguments:

<button live-click="archive" live-data="archived">Archive</button>
06 — Interaction

Targeting the DOM

live-target accepts a plain CSS selector (#result, .row), or one of these relative traversal helpers — the DOM equivalent of jQuery's .closest()/.find()/.parent() family, resolved from the triggering element:

SyntaxResolves to
selfthe triggering element itself
closest(selector)nearest matching ancestor
find(selector)matching descendants
parentdirect parent element
children(selector?)direct children, optionally filtered
next(selector?)next sibling
prev(selector?)previous sibling
siblings(selector?)all siblings, optionally filtered
<tr live-scope="Order/ItemController">
  <td><button
    live-click="remove"
    live-target="closest(tr)"
    live-dom="remove"
  >×</button></td>
</tr>

Multiple selectors can be comma-separated and are matched by index against a comma-separated live-dom.

07 — Interaction

Loading states & callbacks

live-loading / live-loading-indicator

Both accept an element selector to show while the request is in flight, and hide once it settles. Reference counting means two overlapping requests pointed at the same indicator never hide it prematurely.

<!-- show/hide a specific element by selector -->
<button live-click="save" live-loading="#save-spinner">Save</button>

<!-- live-loading="true" falls back to the global ".loading" overlay -->
<button live-click="save" live-loading="true">Save</button>

<!-- live-loading-indicator (no value) toggles itself -->
<button live-click="save" live-loading-indicator>
  <span class="spinner"></span> Saving…
</button>

A page-wide progress bar (fixed to the top of the viewport) also animates automatically on every SPA navigation, independent of these attributes.

live-callback-before

Runs before the request fires. Return (or resolve, for async/Promise-returning callbacks) false to cancel — perfect for confirmation dialogs:

<button live-click="deleteUser" live-callback-before="confirmDelete">Delete</button>

<script>
  function confirmDelete(el) {
    return confirm('Are you sure?'); // false cancels the request entirely
  }
</script>

live-callback-after

Runs once the response has landed, receiving the triggering element and the raw response:

<button live-click="save" live-callback-after="onSaved">Save</button>

<script>
  function onSaved(el, response) {
    toast('Saved!');
  }
</script>

On live-submit forms specifically, both callbacks can also be written as inline expressions (anything containing a () instead of a bare function name.

08 — Reactivity

Live Compute — a spreadsheet inside your HTML

live-compute evaluates a formula entirely in the browser, reading every named input currently on the page — no server round-trip, no network latency. It re-evaluates on every keystroke across a convergence loop (up to 10 iterations, batched within a 16ms time budget) so chains of dependent formulas settle correctly even with 1000+ inputs on a page.

<input name="qty" value="10">
<input name="price" value="25000">
<input name="discount" value="10">

<input
  live-compute="qty * price * (1 - discount / 100)"
  live-compute-format="idr"
  readonly
>

Number formats

live-compute-format controls both how existing values are parsed (so 1.234,56 is read correctly as an IDR-formatted number) and how the computed result is displayed. Built-in formats:

KeyExample outputDefault decimals
idr1.000.0000
usd1,000,000.002
jpy1,000,0000
eur1.000.000,002
percent12.5%1
plain1,000,0000
autoFollows the current global currency — see below.

Omit live-compute-format entirely and the element is never formatted — this is what keeps plain numeric outputs (row indices, counts) safe from being mangled into currency notation. Use live-decimal-max to override the decimal count for a single element regardless of format default.

Global currency switching

Elements pinned to a literal format (idr, usd…) never change on their own. Elements that opt in with live-compute-format="auto" instantly re-render (never re-converting the underlying value) whenever you call:

<script>
  LiveDom.setCurrency('usd');            // only "auto" elements react

  LiveDom.registerFormat('gbp', {        // register a custom currency/format
    kind: 'currency', locale: 'en-GB',
    thousandSep: ',', decimalSep: '.', defaultDecimals: 2
  });

  LiveDom.unpin(document.querySelector('#price')); // drop a pinned format, fall back to "auto"
</script>

Every switch dispatches a livedom:currencychange CustomEvent on document with { from, to } in detail, so you can sync a currency picker UI elsewhere on the page.

Aggregate functions

Six spreadsheet-style functions work inside any live-compute expression. Use a ? wildcard in the field name to aggregate across repeated rows — LiveDomJS scans the page for every input whose name matches the wildcard pattern.

FunctionExample
sumsum(subtotal_?)
avgavg(price_?)
min / maxmax(price_?)
countcount(qty_?)
sumifsumif(status_?, 'paid', total_?)
<!-- Aggregate across all rows sharing the "subtotal_?" pattern -->
<input live-compute="sum(subtotal_?)" live-compute-format="idr" readonly>

<!-- Only sum rows where status_? equals 'paid' -->
<input live-compute="sumif(status_?, 'paid', total_?)" readonly>

Fine-tuning evaluation

live-compute-init="false"

Locks an output element out of the initial calculation pass — it stays untouched until the user actually edits a related input, then unlocks permanently for that page view. Useful for outputs that should keep a server-rendered value until the user interacts.

live-compute-skip="true"

Excludes an element from being read as a formula input, even though it still has a name — handy for a field you want visible/serialized but not part of anyone's formula dependency graph.

live-compute-trigger="qty, price"

Restricts which named fields force this specific element to recompute, instead of any input on the page. Useful when a formula is expensive or has side effects you want minimized.

live-decimal-max="2"

Overrides the number of decimals shown, independent of the format's own default — e.g. show 2 decimals on an otherwise zero-decimal idr field.

Masked inputs (any <input> carrying live-compute-format) re-format live while typing, without ever losing cursor position — the caret is repositioned by digit count, not character index, so inserting a thousands separator mid-keystroke never jumps the cursor.

09 — Reactivity

Reactive directives

These re-evaluate whenever an input inside the enclosing live-scope changes (debounced 200ms). Expressions run against every named input in that scope as variables — checkbox names resolve to their value only when checked, and bracketed names like discount[1] become discount_1 inside the expression.

AttributeEffect
live-showShows/hides the element based on a truthy expression.
live-classAppends the expression's string result to class-base (a separate attribute holding the element's permanent classes).
live-styleSets the element's style attribute from the expression's string result.
live-attrSets/removes one or more arbitrary attributes: attr:expr, attr2:expr2.
live-bindTwo-way mirror: any input named X pushes its value live into every element with live-bind="X".
<div live-show="qty > 0">In stock</div>

<div class-base="font-bold" live-class="total > 1000000 ? 'text-green-500' : 'text-red-500'">
  Total
</div>

<div live-style="'opacity:' + (qty > 0 ? 1 : 0.4)">Preview</div>

<button live-attr="disabled: qty < 1">Checkout</button>

<!-- live-bind: type in one, see it mirrored anywhere -->
<input name="username">
<span live-bind="username"></span>

Expressions are evaluated with Function('ctx', 'with(ctx){ return (...) }') against a scoped object built only from that particular live-scope's inputs — never against window — so a broken formula fails safely (logs a warning in debug mode, returns null) instead of throwing.

10 — Real-time & SPA

live-realtime — WebSocket broadcasting in one attribute

Add live-realtime="true" to any trigger attribute and the request no longer just updates the caller's own DOM — it asks the server to broadcast the update, via Laravel Reverb, to every open page that has a matching live-scope. Each recipient independently re-fetches and re-renders; nobody polls.

<!-- without live-realtime: a normal AJAX request, only this page updates -->
<button live-click="refreshMetrics" live-target="#metrics-panel">Refresh</button>

<!-- with live-realtime: broadcasts to every open dashboard -->
<button
  live-click="refreshMetrics"
  live-realtime="true"
  live-target="#metrics-panel"
>Sync All</button>

What happens under the hood

Client sends X-Live-Reverb: true + realtime: true
    │
    ▼
AjaxController detects the realtime flag → does NOT run the render-and-return path
    │
    ▼
reverbDynamic($controller, $action, $target, ...) dispatches DynamicBroadcastEvent (ShouldBroadcast)
    │
    ▼
Laravel Reverb pushes to the channel over WebSocket
    │
    ▼
dynamic-broadcast.js on every subscribed page matches [live-scope] containing $controller
    │
    ▼
Each page independently re-calls the same controller/action via GET and re-renders its own DOM

Because every recipient re-fetches independently rather than receiving pre-rendered HTML in the broadcast payload, each client always gets output rendered with its own auth/session/locale context.

Broadcasting to specific users (server-side)

Call the reverbDynamic() global helper directly from anywhere in your backend (a job, an observer, another controller) to push an update proactively, not just in response to a client request:

reverbDynamic(
    controller: 'Dashboard/MetricsController',
    function: 'refreshMetrics',
    target: '#metrics-panel',
    data: null,
    typeChannel: 'private',          // 'public' | 'private' | 'presence'
    recipients: [$user->id],
    eventName: 'html-render',
);
typeChannelResulting channel
publicpublic-{recipient} (defaults to realtime-updates if recipients omitted)
privateprivate-user.{recipient}
presencepresence-{recipient}

On the client, every page subscribes automatically to public-realtime-updates and, if a user is authenticated, to private-user.{id} — this is wired up for you inside dynamic-broadcast.js, injected automatically once Reverb is detected.

11 — Real-time & SPA

SPA navigation

Wrap any container in live-spa-region="name" to turn it into an SPA outlet. Links and forms inside it are intercepted automatically — no router configuration, no client-side route table.

<main live-spa-region="main">
  <!-- every <a href> and <form> in here becomes SPA navigation -->
</main>

What gets intercepted

  • <a href> — fetched via GET, response parsed, matching live-spa-region elements swapped by name, history pushed.
  • <form> GET — query string built from form fields, fetched, region swapped, history replaced.
  • <form> POST — submitted via AJAX; a JSON redirect key triggers a follow-up GET automatically; a 422 response renders field errors inline (.is-invalid / .invalid-feedback) instead of navigating.
  • Browser back/forward (popstate) — replays the stored URL through the same SPA loader.

Any <script> tag inside a freshly swapped region is re-executed (external scripts once per src, inline scripts every time, each isolated in an IIFE) — so page-specific initialization code keeps working after navigation exactly like a full page load.

Excluding URLs from SPA handling

Set window.liveDomConfig.spaExcludePrefixes before LiveDomJS initializes to let specific paths fall through to a normal full-page navigation (e.g. a download link or an external auth redirect):

<script>
  window.liveDomConfig = { spaExcludePrefixes: ['/downloads', '/logout'] };
</script>

Lifecycle events

EventFires
live-dom:initOnce, after LiveDomJS finishes its own initial bootstrap.
live-dom:afterUpdateAfter any AJAX-driven DOM update (rebinds all delegated handlers, safe to listen to for custom widgets).
live-dom:afterSpaAfter an SPA region swap, with detail.url.
<script>
  document.addEventListener('live-dom:afterSpa', (e) => {
    console.log('Navigated to', e.detail.url);
  });
</script>
12 — Platform

Errors & debugging

LiveDomJS reads Laravel's own app.debug flag (surfaced to the browser as a <meta name="app-debug"> tag) to decide how failures are shown, and keeps an in-memory history of every error it has displayed during the session.

app.debug = true

A detailed modal appears with the exception message, file, line, and trace — parsed from either a Laravel Ignition HTML page or a JSON exception payload. It includes quick "copy" and "copy prompt for AI" actions for pasting into an assistant.

app.debug = false

A clean, dismissible toast shows a generic message — no stack trace, file paths, or internal detail ever reaches production users.

Laravel validation errors (HTTP 422 with a JSON errors object) are handled specially on SPA form submits: each field gets an .is-invalid class and an inline .invalid-feedback message next to it, instead of triggering the generic error modal.

Every AJAX request also auto-cancels its predecessor sharing the same target — clicking "search" three times fast only ever renders the last response, never a stale one that happened to resolve out of order.

13 — Platform

Configuration reference

Publish config/livedomjs.php with php artisan livedomjs:install --publish to edit any of these:

KeyDefaultPurpose
route_prefixajaxThe dynamic endpoint becomes /{prefix}/{controller}/{action}.
route_middleware['web']Middleware stack applied to the dynamic AJAX route.
auto_injecttrueAuto-injects livedom.js (100% vanilla JS — no jQuery required) before </body> on every HTML response.
auto_inject_realtimetrueWhen true and a Reverb key is configured, also injects the CSRF meta tag, window.userGlobal, Echo, and dynamic-broadcast.js.
jquery_cdnnullNot needed by LiveDomJS itself. Set a CDN URL only if some other custom script in your project still depends on a global jQuery.
pusher_cdnpusher-js 8.4.0 CDNReverb speaks the Pusher protocol, so the client library is pusher-js.
echo_cdnlaravel-echo 1.16.1 CDNSet either CDN key to null if you bundle your own via Vite.
serve_assetstrueServes livedom.js/dynamic-broadcast.js from the package directly, no vendor:publish required. Published files in public/ always take priority.
14 — Platform

JavaScript API

These are exposed globally for cases the declarative attributes don't cover.

APIPurpose
LiveDom.setCurrency(code)Switches the global "auto" number format instantly, no re-conversion of values.
LiveDom.registerFormat(key, cfg)Registers a new named format/currency (or overrides a built-in one).
LiveDom.unpin(el)Removes a literal live-compute-format from an element so it follows the global "auto" format.
LiveDom.config.currencyRead-only view of the currently active global currency key.
runAjaxRequest(...)Same request pipeline the attribute engine uses internally — call it directly from your own JS for advanced flows.
debouncedAjaxDynamic(...)Debounced wrapper around the core AJAX call, keyed per controller+method.
autoBindDomFromResponse(data)Given a plain object response, writes each key into any matching #id/.class on the page (camelCase, kebab-case, and snake_case are all matched).
15 — Platform

PHP API

Controller name resolution

live-scope values map to App\Http\Controllers\* by splitting on / or . and ucfirst-ing each segment:

live-scope="Invoice/ItemController"   →  App\Http\Controllers\Invoice\ItemController
live-scope="UserController"           →  App\Http\Controllers\UserController

Both segments are validated with a whitelist regex (/^[a-zA-Z0-9\/\.]+$/ for the controller, /^[a-zA-Z0-9]+$/ for the action) before the class is even referenced. A missing class or method returns a clean 404 JSON response rather than a fatal error.

reverbDynamic()

function reverbDynamic(
    string $controller,
    string $function,
    string $target,
    $data = null,
    string $typeChannel = 'public',
    array $recipients = [],
    string $eventName = 'html-render',
): void

Globally available (autoloaded via composer.json's files section) — call it from anywhere in your app, not only from within an live-realtime request cycle.

Blade component

If you ever set auto_inject to false for manual control, drop this into your layout instead:

<x-livedomjs::livedom-scripts />
16 — Reference

Full attribute reference

Every live-* attribute LiveDomJS understands, in one table.

Structure & scope

live-scopeController path + request/data boundary.
live-targetWhere the response is applied — selector or traversal DSL.
live-domHow the response is applied (html, append, value, remove, …).
live-dataFixed payload, overrides scope serialization.
live-methodOverride HTTP verb (GET/POST/PUT/DELETE…).

Interaction triggers

live-clickCall method(s) on click.
live-changeCall method(s) on change.
live-inputCall method(s) on input, debounced.
live-keyupCall method(s) on keyup, debounced.
live-submitCall method(s) on form submit (auto-prevents default).
live-hoverCall method(s) on delegated mouseenter.
live-pollRepeat the element's live-click method every N ms.

Feedback & lifecycle

live-loadingSelector (or "true") to show/hide while the request is in flight.
live-loading-indicatorSame as above, defaults to the triggering element itself.
live-callback-beforeRuns before the request; returning/resolving false cancels it.
live-callback-afterRuns after the response lands, receives (element, response).
live-realtimeBroadcast this action to every matching open page via Reverb.

Live Compute

live-computeClient-side formula, re-evaluated live.
live-compute-formatNumber format for parsing & display (idr, usd, jpy, eur, percent, plain, auto, or custom).
live-decimal-maxOverrides decimal places for this element only.
live-compute-init"false" skips the initial calculation pass until the user edits something.
live-compute-skip"true" excludes this field from being read as a formula input.
live-compute-triggerComma-separated field names that force this element to recompute.

Reactive directives

live-showToggle visibility from an expression.
live-classAppend a dynamic class from an expression.
live-styleSet inline style from an expression.
live-attrSet/remove one or more attributes from expressions.
live-bindMirror an input's value into any element bound to its name.

SPA

live-spa-regionNamed SPA outlet — links/forms inside it navigate without a full reload.
17 — Reference

Recipes

Invoice with auto-calculation

<div live-scope="Invoice/ItemController">
  <table>
    <tr>
      <td><input name="qty" value="1"></td>
      <td><input name="price" value="150000"></td>
      <td><input name="discount" value="0"></td>
      <td><input
        live-compute="qty * price * (1 - discount / 100)"
        live-compute-format="idr" readonly></td>
      <td><button live-click="removeItem" live-target="closest(tr)" live-dom="remove">×</button></td>
    </tr>
  </table>

  <div>Total:
    <input live-compute="sum(subtotal_?)" live-compute-format="idr" readonly>
  </div>

  <button live-click="submit" live-target="#response">Submit Invoice</button>
  <div id="response"></div>
</div>

Debounced search-as-you-type

<div live-scope="Catalog/ProductController">
  <input name="q" live-input="search" live-target="#results" placeholder="Search products…">
  <div id="results"></div>
</div>

Real-time collaborative dashboard

<div live-scope="Dashboard/MetricsController">
  <div id="metrics-panel">{{-- server-rendered on load --}}</div>
  <button live-click="refreshMetrics" live-realtime="true" live-target="#metrics-panel">
    Sync All
  </button>
</div>

SPA app shell

<body>
  <nav>
    <a href="/dashboard">Dashboard</a>
    <a href="/reports">Reports</a>
  </nav>

  <main live-spa-region="main">
    @yield('content')
  </main>
</body>

As long as every page you navigate to also renders a live-spa-region="main" wrapper with the same name, back/forward and direct links all keep working — no client-side route table to maintain.

18 — Reference

Troubleshooting

"Element with live-click needs a live-scope attribute on an ancestor"

Every trigger attribute that calls a controller method needs a live-scope somewhere above it in the DOM. If the element only performs a local DOM action (no method name), you can omit live-scope entirely — pass an empty live-click to trigger live-dom/live-target without a server call.

My aggregate formula always returns 0

Aggregate functions take exactly one field pattern — sum(a, b) is read as a single (nonexistent) field named literally "a, b", not two fields added together. Use the wildcard pattern instead: sum(price_?) against fields named price_1, price_2, etc. In debug mode, a console warning flags this exact mistake.

live-realtime does nothing

Real-time is silently inactive until Laravel Reverb (or another broadcasting driver populating config('broadcasting.connections.reverb.key')) is configured — this is intentional, so projects that don't need it never see an "Echo is not defined" error. Run php artisan install:broadcasting (Laravel 11/12) and keep reverb:start + queue:work running.

A field outside my table row leaked into the request

Check that each repeating unit (row, card) has its own live-scope. Only inputs inside the nearest live-scope ancestor of the trigger are serialized — a shared outer scope will pull in every row's inputs at once.

A currency-formatted input shows the wrong number after typing

Make sure live-compute-format matches how the value was already written into the page (server-rendered or otherwise) — the parser trusts the format you declare to know which character is the decimal separator versus the thousands separator.

19 — Reference

How it compares

LiveDomJSLivewireHTMX
New files per feature02+ (PHP class + Blade)0
Route registration requiredNoNoYes
Client-side calculationsBuilt-inServer round-tripManual JS
Real-time (WebSocket)One attributeComplex setupExtension required
Laravel-nativeYesYesNo
Build step requiredNoNoNo
jQuery dependencyNoNoNo

Best fit: data-heavy Laravel apps (ERP, CRM, admin panels, internal tools) that want reactive UI without adopting a full component framework.

20 — Reference

Changelog & roadmap

Recent since the last docs pass

  • Global currency switching — LiveDom.setCurrency(), registerFormat(), unpin(), and the "auto" format.
  • Cursor-safe live input masking for currency-formatted fields while typing.
  • live-decimal-max, live-compute-skip, and live-compute-trigger for fine-grained control over the compute engine.
  • Unified, abortable SPA request pipeline — GET navigation, form GET, and form POST now share one controller, so a stale in-flight navigation can never render over a newer one.
  • Redesigned dev-mode error modal with copy/AI-prompt actions and an in-session error history.
  • Reference-counted live-loading / live-loading-indicator, safe for overlapping requests.
  • Laravel 11/12 "slim skeleton" support — real-time detection no longer assumes config/broadcasting.php exists.

Shipped foundation

  • ✅ Attribute-driven AJAX interactions (click, change, input, keyup, submit, hover, poll)
  • ✅ Scope-based data isolation (live-scope)
  • ✅ Live compute with aggregate functions (sum, avg, min, max, count, sumif)
  • ✅ Real-time broadcasting via Laravel Reverb
  • ✅ SPA navigation with pushState/popstate support
  • ✅ Reactive directives (show, class, style, attr, bind)
  • ✅ Remove the jQuery dependency

On the roadmap

  • ⬜ DevTools browser extension
  • ⬜ VS Code extension for attribute autocomplete
  • ⬜ Official testing utilities

Contributing

Bug reports and feature suggestions are welcome. Before submitting changes, be ready to explain what you're changing, what could break, and how it affects the framework's long-term direction.

git clone https://github.com/GadingRengga/LiveDomJs
cd LiveDomJs
git checkout -b feature/your-feature-name

Author

Gading Rengga

github.com/GadingRengga

gading.rengga@gmail.com

Licensed under the MIT License. Free for personal and commercial use.