cDOM (Computational DOM)
A declarative, expression-based way to build reactive UIs.
cDOM and JPRX are currently in an experimental phase. The expression syntax, helper functions, and integration patterns are subject to change as we continue to evolve the library. Use with caution in production environments.
Overview
The Computational DOM (cDOM) is a way to describe user interfaces using reactive expressions that feel as natural as spreadsheet formulas. Instead of writing JavaScript logic to update your UI, you define the relationships between your data and your elements.
cDOM was designed so that both developers and LLMs can generate safe user interfaces that humans can interact with across platforms.
cDOM uses JPRX (JSON Pointer Reactive eXpressions) as its expression language. JPRX extends JSON Pointer (RFC 6901) with reactivity, relative paths, and helper functions. cDOM also supports standard XPath for powerful DOM navigation during element construction. Together with deep integration for JSON Schema (Standard Draft 7+), cDOM provides industrial-strength data validation and automatic type coercion.
Simple Example
Here's a simple counter in cDOM. Notice how the UI is purely declarative — no JavaScript logic:
await import('/lightview-cdom.js');
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
const { $ } = Lightview;
const cdom = `{
div: {
onmount: =state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this }),
children: [
{ h2: "Counter" },
{ p: ["Count: ", =/local/count] },
{ button: { onclick: =++/local/count, children: ["+"] } },
{ button: { onclick: =--/local/count, children: ["-"] } }
]
}
}`;
$('#example').content(hydrate(parseJPRX(cdom)));
This defines a counter with:
onmount— A Lightview lifecycle hook where we initialize state.=state(...)— An initializer that creates local reactive state.scope: $this— Explicitly attaches the state to the current element.=/local/count— A path that reactively displays the count value.
The UI automatically updates whenever count changes — no manual DOM manipulation required.
Using XPath
cDOM allows elements to navigate and reference the DOM structure during construction using standard XPath. This is strictly a cDOM feature (not JPRX) used for structural navigation.
XPath is incredibly useful for keeping your definitions DRY (Don't Repeat Yourself) by referencing existing attributes instead of repeating values.
await import('/lightview-cdom.js');
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
const { $ } = Lightview;
const cdom = `{
div: {
id: "profile-container",
class: "card",
"data-theme": "dark",
children: [
{ h3: "User Profile" },
{ button: {
id: "7",
// XPath #../@id gets the "7" from this button's id
// XPath #../../@id gets "profile-container" from the g-parent div
children: ["Button ", #../@id, " in section ", #../../@id]
}}
]
}
}`;
$('#example').content(hydrate(parseJPRX(cdom)));
In the example above, the button's text is derived entirely from its own id and its
parent's id using #../@id and #../../@id.
For more details, see the Full XPath Documentation.
Advantages
🛡️ Enhanced Security
cDOM strictly avoids eval() and direct HTML injection. By using a custom
high-performance parser and a registry of pre-defined helper functions, it provides a safe sandbox
for dynamic content, making it highly resistant to XSS attacks.
🤖 LLM Friendly
Large Language Models excel at generating structured data and formulaic expressions. cDOM's declarative nature and concise syntax make it far easier for AI to generate correct, bug-free UI components compared to traditional JavaScript-heavy frameworks.
JPRX (JSON Pointer Reactive eXpressions)
JPRX is the expression language that powers cDOM. It extends JSON Pointer (RFC 6901) with reactivity, relative paths, and helper functions.
Delimiters
JPRX expressions begin with a = delimiter:
| Delimiter | Purpose | Example |
|---|---|---|
=/ |
Access a path in the global registry | =/users/0/name |
=function( |
Call a helper function | =sum(/items...price) |
Once inside a JPRX expression, paths follow JSON Pointer syntax. The =
is only needed at the start of the expression for paths or function names.
Escaping the = Delimiter
When using oDOM or vDOM (Object DOM), any string value starting with
= is interpreted as a JPRX expression. If you need a literal string that begins with
an equals sign (e.g., a mathematical equation or status message), you can escape it by prefixing
with a single quote:
// Interpreted as JPRX expression:
{ "p": "=/user/name" } // Resolves the path /user/name
// Escaped to produce a literal string:
{ "p": "'=E=mc²" } // Renders as "=E=mc²"
{ "p": "'=42" } // Renders as "=42"
The single-quote escape ('=) at the start of a string tells the parser to treat
the rest of the string (including the =) as literal content.
Anatomy of a Path
Inside a JPRX expression, paths follow JSON Pointer with these extensions:
| Path | Description | Origin |
|---|---|---|
/users/0/name |
Absolute path (property access) | JSON Pointer |
/items[2] |
Array index (bracket notation) | JPRX extension |
./child |
Relative to current context | JPRX extension |
../sibling |
Parent context access | JPRX extension |
/items...price |
Extract price from all items (explosion) |
JPRX extension |
Function Calls
Paths can contain function calls to transform data:
=currency(sum(map(filter(/orders, eq(_/status, 'paid')), _/total)...))
- /orders: Access the
orderscollection (JSON Pointer) - filter(..., eq(...)): Keep only "paid" orders
- map(..., _/total): Extract the
totalfrom each order - ...: Explode the array into individual arguments
- sum(...): Add up all the totals
- =currency(...): Format as currency string
Placeholders
| Placeholder | Description | Example |
|---|---|---|
_ |
Current item during iteration | =map(/items, _/name) |
$event |
Event object in handlers | =set(=/selected, $event/target/value) |
Comparison to Excel
Think of your UI as a spreadsheet. In Excel, if Cell C1 has the formula =A1+B1, C1
updates automatically whenever A1 or B1 changes.
cDOM brings this exact paradigm to the web. Every attribute and text node can be a "cell" that computes its value based on other "cells" (reactive signals).
| Feature | Excel | cDOM / JPRX |
|---|---|---|
| Reactive Unit | Cell | Signal / State Proxy |
| Formulas | =SUM(A1:A10) |
=sum(/items...price) |
| Path Resolution | Cell References (A1, $B$2) | JSON Pointer paths (./name, =/global) |
| Recalculation | Automatic on change | Automatic on change |
Lightview Integration
cDOM integrates seamlessly with Lightview's existing DOM formats. You can use JPRX expressions
in any of Lightview's hypermedia formats just by loading lightview-cdom.js:
vDOM (Virtual DOM)
JPRX expressions work directly in vDOM arrays or objects:
// vDOM formal object
{
tag: "div",
attributes: { class: "counter" },
children: [
{ tag: "p", children: ["Count: ", "=/local/count"] },
{ tag: "button", attributes: { onclick: "=++/local/count }, children: ["+"] }
]
}
oDOM (Object DOM)
JPRX expressions work in oDOM objects:
{
div: {
class: "counter",
children: [
{ p: { children: ["Count: ", "=/local/count] } },
{ button: { onclick: "=++/local/count", children: ["+"] } }
]
}
}
cDOM Shorthand with JPRX
cDOM's concise shorthand syntax. Note that cDOM does not require quoting attribute names or JPRX expressions like vDOM and oDOM do and you can include comments:
{
div: {
class: "counter",
// This is a JPRX comment
children: [
{ p: ["Count: ", =/local/count] },
{ button: { onclick: =++/local/count, children: ["+"] } }
]
}
}
DOM Patches & Decentralized Layouts
cDOM supports Decentralized Layouts, allowing components to "move themselves" to their rightful home in the DOM upon being created. This is especially powerful for LLM-driven streaming UIs.
=move(target, location?)
The =move helper (typically used in onmount) teleports the host element to a
different part of the document.
{
div: {
id: "weather-widget",
onmount: =move('#sidebar', 'afterbegin'),
content: "Sunny, 75°F"
}
}
Identity & Patching
If the moving element has a unique id and an element with that same ID already exists at the
destination,
the existing element is replaced. This turns =move into an idempotent
patch
command — simply stream the new version of the component with the same ID, and it will update the UI
automatically.
Placement Locations
| Location | Result |
|---|---|
inner / shadow |
Replaces all children of the target. |
afterbegin / prepend |
Inserts at the start of the target. |
beforeend / append |
Inserts at the end of the target (default). |
outer / replace |
Replaces the target node itself. |
beforebegin / afterend |
Inserts before or after the target node. |
State & Binding
cDOM does not use attribute directives for state it uses lifecycle events and helpers instead.
Lifecycle State
In Lightview, you initialize state within the onmount hook. The =state and
=signal helpers accept an options object where you can specify a scope,
as well as schema requirements.
Using a Registered Schema
// 1. Register centrally in JS
Lightview.registerSchema('User', { name: 'string', age: 'number' });
// 2. Use in cDOM
{ "onmount": "=state({}, { name: 'profile', schema: 'User', scope: $this })" }
Standard Schema Behaviors
When using the schema option, you can use these standard behaviors:
- "auto": Infers a fixed schema from the initial value. Strict type checking (throws on mismatch).
- "dynamic": Like auto, but allows adding new properties to the state object.
- "polymorphic": The most powerful setting. It includes "dynamic" behavior and automatically coerces values to match the inferred type (e.g., "50" -> 50).
Example: Polymorphic Coercion
{
div: {
"onmount": "=state({ count: 0 }, {
name: 'local',
schema: 'polymorphic',
scope: $this
})",
children: [
{ p: ["Typing '10' into a bind will save it as the number 10."] }
]
}
}
By scoping the state to the element, you can create multiple independent instances of components. Lightview uses a high-performance up-tree search to resolve these names.
Two-Way Binding ($bind)
Two-way data binding is achieved via the =bind(path) helper.
{ input: { type: "text", value: "=bind(/profile/name)", placeholder: "Enter name" } }
{ input: { type: "checkbox", checked: "=bind(/settings/enabled)" } }
Handling Transformations
Because =bind is strict (it only accepts direct paths), you cannot pass a computation
like =bind(upper(/name)). To transform data during binding, you have two choices:
-
Manual Transformation: Use an
oninputhandler:{ oninput: "=set(/name, upper($event/target/value))" } -
Schema Transformation: Define a
transformin your schema:The state manager will automatically apply the transformation during the write-back phase ofLightview.registerSchema('Profile', { name: { type: 'string', transform: 'upper' } });=bind.
Shopping Cart Example
A more complete example showing reactive expressions with data transformations:
// Shopping Cart: Demonstrating $map and $currency helpers
await import('/lightview-cdom.js');
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
const { $ } = Lightview;
const cdomString = `{
div: {
"onmount": "=state({
cart: {
items: [
{ name: 'Apple', price: 1.00 },
{ name: 'Orange', price: 2.00 }
]
}
}, 'store')",
children: [
{ h3: "Shopping Cart" },
{ ul: {
children: =map(/store/cart/items, { li: { children: [_/name, " - ", currency(_/price)] } })
}},
{ p: {
style: "font-weight: bold; margin-top: 1rem;",
children: ["Total: ", =currency(sum(/store/cart/items...price))]
}}
]
}
}`;
const hydrated = hydrate(parseJPRX(cdomString));
$('#example').content(hydrated);
Interactive Example
Choose a syntax to see how the same reactive counter can be defined using standard JSON, concise JPRXC, or operator syntax.
// JPRX: Standard JSON format (strict)
await import('/lightview-cdom.js');
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
const { signal, $ } = Lightview;
const count = signal(0, 'count');
const cdomString = `{
"div": {
"children": [
{ "h3": ["Standard JPRX Counter"] },
{ "p": { "children": ["Count: ", "=/count"] }},
{ "div": { "children": [
{ "button": { "onclick": "=decrement(/count)", "children": ["-"] } },
{ "button": { "onclick": "=increment(/count)", "children": ["+"] } }
]}}
]
}
}`;
const hydrated = hydrate(parseJPRX(cdomString));
$('#example').content(hydrated);
globalThis.LightviewCDOM.activate(hydrated.domEl);
Operator Syntax
JPRX supports prefix and postfix operator syntax as alternatives to function calls. This makes expressions more concise and familiar to JavaScript developers.
Equivalent Expressions
| Function Syntax | Operator Syntax | Description |
|---|---|---|
=increment(/count) |
=++/count or =/count++ |
Increment by 1 |
=decrement(/count) |
=--/count or =/count-- |
Decrement by 1 |
=toggle(/enabled) |
=!!/enabled |
Toggle boolean (prefix only) |
Events and Interaction
cDOM handles user interactions gracefully, whether in standalone applications or LLM-driven environments.
Manual Implementation
Use standard event attributes with JPRX expressions:
{ button: { onclick: =++/count, children: ["Click Me"] } }
{ input: { oninput: =set(/name, $event/target/value) } }
LLM-Generated Interaction
LLMs can generate event handlers to register interest in user actions or trigger UI updates:
// 1. Notify LLM of an event
{ button: { onclick: =fetch('/api/notify', { method: 'POST', body: $event }), children: ["Notify"] } }
// 2. Request a UI Patch from LLM
{ button: { onclick: =mount('/api/update', { method: 'POST', body: $event }), children: ["Update UI"] } }
The $event placeholder gives the LLM full context of the interaction. When using
=mount, the server should respond with cDOM, vDOM, or oDOM content (see the
Network section for more details).
By default, =mount will append the response to the body and
then let it "rip itself out" and teleport to its final destination if the response
contains a =move helper. This "Safe Landing" strategy ensures decentralized layouts
work seamlessly without the patcher needing to know the exact destination.
The Interaction Lifecycle
🤖 LLM-Driven Flow
- LLM generates UI structure as JSON
- Server serves the JSON to the client
- Client renders cDOM and activates reactivity
- User interacts (e.g., clicks a button)
- Client sends interaction data to server
- Server relays event to LLM
- LLM sends UI patch back
- Client merges update, UI refreshes
🏠 Standalone Flow
- cDOM defined in source code
- Client activates UI on page load
- User interacts (e.g., toggles a switch)
- State updates immediately
- UI recalculates reactively (0 latency)
- Server only for persistence/APIs
JavaScript API
Interact with cDOM programmatically using the LightviewCDOM global object.
activate(root)
Scans the DOM from root (defaults to document.body) and
initializes all cDOM directives.
LightviewCDOM.activate();
LightviewCDOM.activate(document.getElementById('myApp'));
hydrate(object)
Converts $-prefixed strings into reactive computed signals.
const config = { title: "Dashboard", total: "=/cart/items...price" };
const liveConfig = LightviewCDOM.hydrate(config);
parseJPRX(string)
Parses cDOM's concise syntax (unquoted keys, JPRX expressions) into a JavaScript object.
const obj = LightviewCDOM.parseJPRX(`{ div: { children: [=/name] } }`);
registerSchema(name, definition)
Registers a reusable schema for state validation and initialization. Supported behaviors include
"auto" (strict/fixed), "dynamic" (strict/expandable), and
"polymorphic" (coerce/expandable).
Lightview.registerSchema('User', { name: 'string', age: 'number' });
// Usable in JPRX: =state({}, { name: 'profile', schema: 'User' })
registerHelper(name, fn, options?)
Registers a custom helper function for use in JPRX expressions.
LightviewCDOM.registerHelper('double', (x) => x * 2);
// Now usable: =double(/count)
JPRX Helper Functions
cDOM includes a rich set of built-in helpers for common transformations.
For security, only registered helpers are available — no access to globalThis.
Math
Basic arithmetic operations.
+, add, -, sub, *, mul, /, div, round, ceil, floor, abs, mod, pow, sqrt
Stats
Aggregate calculations.
sum, avg, min, max, median, stdev, var
String
Text manipulation.
upper, lower, trim, capitalize, titleCase, contains, startsWith, endsWith, replace, split, len, join, concat, default
Array
Collection processing.
count, map, filter, find, unique, sort, reverse, first, last, slice, flatten, join, len, length
Logic & Comparison
Boolean logic and comparisons.
Named Operators
if, and, or, not, eq, neq, gt, lt, gte, lte, between, in
Aliases
&&, ||, !, ==, ===, !=, >, <, >=, <=
Example: =if(gt(=/count, 10), 'Large', 'Small')
Conditional Aggregates
Statistical functions with predicates.
sumIf, countIf, avgIf
Formatting
Display formatting.
number, currency, percent, thousands
Explosion Operator: In JPRX, ... is placed at the end of a path
(e.g., =/items...price) to expand arrays into arguments.
DateTime
Date operations.
now, today, date, formatDate, year, month, day, weekday, addDays, dateDiff
Lookup
Data retrieval from indexed structures.
lookup, vlookup, index, match
State & Lifecycle
Initialize and modify reactive state.
state, signal, bind, set, increment (++), decrement (--), toggle (!!), push, pop, assign, clear
Network
HTTP requests.
fetch(url, options?)
mount(url, options?)
DOM & XPath
Structural navigation and manipulation. For fetching and mounting remote content, see the mount() helper in the Network section.
move(selector, location?), xpath(expression)
While move is for local placement, mount is used for remote Hypermedia updates.
It fetches content (cDOM, vDOM, or oDOM) and injects it into the DOM. If the content contains a
=move helper, it will automatically relocate itself upon mounting.
| Option | Default | Description |
|---|---|---|
method |
"GET" |
HTTP method (GET, POST, etc.) |
body |
undefined |
Request body. If an object, it is automatically stringified to JSON and the
Content-Type is set to application/json.
|
headers |
{} |
Additional HTTP headers. |
target |
"body" |
(mount only) CSS selector for destination. |
location |
"beforeend" |
(mount only) Placement relative to target. |
xpath(expression): Returns a reactive computed signal based on the DOM structure
at evaluation time. Note that full MutationObserver reactivity is currently a TODO.
For static DOM navigation, use the # prefix in cDOM.