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": {
        "id": "Counter",
        "onmount": "=state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this })",
        "children": [
            { "h2": "#../../@id" }, // XPath shorthand: text node -> h2 -> div
            { "p": ["Count: ", "=/local/count"] },
            { "button": { "onclick": "=++/local/count", "children": ["+"] } },
            { "button": { "onclick": "=--/local/count", "children": ["-"] } }
        ]
    }
}`;

$('#example').content(hydrate(parseJPRX(cdom)));

This defines a counter with:

Note: In cDOM, # is the prefix for XPath expressions that navigate and extract data from the DOM, while = is the prefix for JPRX expressions that navigate or apply functions to reactive state.

The UI automatically updates whenever count changes — no manual DOM manipulation required. It also uses XPath to navigate the DOM structure during construction.

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.

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 = `{
    // The cdom below uses a compressed syntax. It does not require quotes 
    // around properties or JPRX/XPath values and supports comments. 
    // It is JSON-like, but not JSON.
    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.

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)

Explosion & Mapping (...)

The ... operator is a powerful JPRX extension used for mapping properties from arrays and spreading array elements as function arguments.

Mapping & Auto-Explosion: path...property

When used between a path to an array and a property name, it acts as a shorthand for mapping. It extracts the specified property from every object in the array. Inside a function call, it automatically spreads (explodes) the resulting values as individual arguments.

// Correct: Maps 'price' and automatically spreads results as arguments to sum()
=sum(/cart/items...price)

// Mapping also works for direct display (returns an array)
{ p: ["Prices: ", =/cart/items...price] }

Argument Spreading: path...

When an array path is followed by a trailing ... at the end of the path expression (and no property name follows), the array elements are spread as individual arguments. Use this when you have a direct reference to an array.

// Passes each number in the array as a separate argument
=sum(/listOfNumbers...)
Warning: Do not combine infix mapping with trailing dots. /items...price... is invalid because the trailing dots are interpreted as part of the property name (looking for a property literally named "price...").

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 allow you to create reactive data that is tied to the DOM structure.

Scope & Name Resolution

When JPRX encounters a name (e.g., =/profile/name), it doesn't just look in a global registry. Instead, it performs an up-tree search:

  1. It starts at the element where the expression is defined.
  2. It looks for a registered signal or state with that name.
  3. If not found, it moves to the parent element and repeats the search.
  4. This continues all the way to the document root, finally checking the global registry if needed.

The $this Placeholder

The $this keyword is a special placeholder that represents the current element. When used in the scope option of =state or =signal, it instructs Lightview to register the name specifically at that element's level in the DOM.

// Scoping state to the current element creates a local namespace
{ "onmount": "=state({ count: 0 }, { name: 'local', scope: $this })" }

This mechanism is what allows you to create reusable components. Each instance of a component can have its own "local" state because the search for local will stop at the first element it finds that has it registered—typically the root of the component instance.

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."] }
        ]
    }
}

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 cDOMC, or operator syntax.

// JPRX: Standard JSON format (strict)
await import('/lightview-cdom.js');
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
const { $ } = Lightview;

const cdomString = `{
    "div": {
        onmount: "=signal(0, 'count')",
        "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);

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.