cDOM (Computational DOM)

A declarative, expression-based way to build reactive UIs.

⚠️
Experimental Feature

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:

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)...))

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:

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:

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

  1. LLM generates UI structure as JSON
  2. Server serves the JSON to the client
  3. Client renders cDOM and activates reactivity
  4. User interacts (e.g., clicks a button)
  5. Client sends interaction data to server
  6. Server relays event to LLM
  7. LLM sends UI patch back
  8. Client merges update, UI refreshes

🏠 Standalone Flow

  1. cDOM defined in source code
  2. Client activates UI on page load
  3. User interacts (e.g., toggles a switch)
  4. State updates immediately
  5. UI recalculates reactively (0 latency)
  6. 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.