lightview 2.3.5 → 2.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gemini/XPATH_IMPLEMENTATION.md +190 -0
- package/_headers +5 -0
- package/_routes.json +7 -0
- package/build-bundles.mjs +10 -0
- package/docs/articles/calculator-no-javascript-hackernoon.md +283 -0
- package/docs/articles/calculator-no-javascript.md +290 -0
- package/docs/articles/part1-reference.md +236 -0
- package/docs/calculator.cdomc +77 -0
- package/docs/calculator.css +316 -0
- package/docs/calculator.html +5 -410
- package/functions/_middleware.js +20 -3
- package/jprx/helpers/dom.js +69 -0
- package/jprx/helpers/logic.js +9 -3
- package/jprx/index.js +1 -0
- package/jprx/package.json +1 -1
- package/jprx/parser.js +359 -80
- package/lightview-all.js +459 -72
- package/lightview-cdom.js +452 -69
- package/lightview-x.js +4 -3
- package/lightview.js +14 -0
- package/package.json +2 -2
- package/src/lightview-cdom.js +150 -7
- package/src/lightview-x.js +5 -2
- package/src/lightview.js +19 -0
- package/test-xpath-preprocess.js +18 -0
- package/test-xpath.html +63 -0
- package/test_error.txt +0 -0
- package/test_output.txt +0 -0
- package/test_output_full.txt +0 -0
- package/tests/cdom/operators.test.js +141 -0
- package/wrangler.toml +1 -0
- package/start-dev.js +0 -93
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Building a Fully Functional Calculator with Zero JavaScript Logic
|
|
2
|
+
|
|
3
|
+
*How cDOM and JPRX let you build complex UIs using pure declarative JSON*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Last time, I showed how [cDOM lets you build simple reactive UIs with JSON](https://medium.com/@anywhichway/building-reactive-uis-with-json-no-javascript-required-b7b1c4a45321). But let's be honest: counters and shopping carts are easy. Almost any framework looks good when the logic is trivial.
|
|
8
|
+
|
|
9
|
+
To see if a declarative, JSON-based approach actually holds up in the real world, you need a problem with messy state, edge cases, and history.
|
|
10
|
+
|
|
11
|
+
You need a calculator.
|
|
12
|
+
|
|
13
|
+
Calculators are tricky because they're full of hidden complexity:
|
|
14
|
+
- Managing input modes (are we typing a new number or replacing the result?)
|
|
15
|
+
- Chaining operations (what happens when you hit `+` then `-`?)
|
|
16
|
+
- DOM interaction (how do we avoid writing 10 separate handlers for buttons 0-9?)
|
|
17
|
+
|
|
18
|
+
This is how I built a fully functional, iOS-style calculator using **zero custom JavaScript functions**—just declarative cDOM and JPRX expressions.
|
|
19
|
+
|
|
20
|
+
## The Result
|
|
21
|
+
|
|
22
|
+
Here is what we are building. A clean, glassmorphic interface where every interaction—from typing decimals to chaining operations—is handled by the expression language itself.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## The State Machine
|
|
27
|
+
|
|
28
|
+
First, we need to model the calculator's brain. It's not just a "current number"; it's a small state machine.
|
|
29
|
+
|
|
30
|
+
We define this state right in the JSON when the component mounts:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
onmount: =state({
|
|
34
|
+
display: "0", // What you see on screen
|
|
35
|
+
expr: "", // History string (e.g., "8 + 5 =")
|
|
36
|
+
prev: "", // The value stored before an operation
|
|
37
|
+
op: "", // The active operator (+, -, *, /)
|
|
38
|
+
waiting: false // True when expecting new number input vs an operator
|
|
39
|
+
}, { name: "c", schema: "polymorphic", scope: $this })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This creates a reactive state object named `c`. We can read it anywhere with paths like `/c/display`.
|
|
43
|
+
|
|
44
|
+
## Solving the "10 Buttons" Problem
|
|
45
|
+
|
|
46
|
+
In a typical framework, you'd loop over an array of numbers to generate buttons, or write a generic `handleNumberClick` function to avoid repetitive code.
|
|
47
|
+
|
|
48
|
+
In JPRX, we can just use the DOM itself.
|
|
49
|
+
|
|
50
|
+
By assigning an `id` to each button, we can write **one single logic expression** that works for every number key. All we have to do is reference `$this.id`:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
{ button: {
|
|
54
|
+
id: "7",
|
|
55
|
+
class: "btn btn-number",
|
|
56
|
+
onclick: =set(/c, {
|
|
57
|
+
display: if(/c/waiting,
|
|
58
|
+
$this.id,
|
|
59
|
+
if(eq(/c/display, "0"),
|
|
60
|
+
$this.id,
|
|
61
|
+
concat(/c/display, $this.id)
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
waiting: false
|
|
65
|
+
}),
|
|
66
|
+
children: ["7"]
|
|
67
|
+
} }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Here's the breakdown of that expression:
|
|
71
|
+
1. **Are we waiting for new input?** (e.g., after hitting `+`) → Replace the display with the button's ID.
|
|
72
|
+
2. **Is the current display "0"?** → Replace it (to avoid "07").
|
|
73
|
+
3. **Otherwise:** → Append the button's ID to the current display string.
|
|
74
|
+
|
|
75
|
+
This works identically for every number button. No loops, no external helper functions.
|
|
76
|
+
|
|
77
|
+
## Operators and "Indirect" Values
|
|
78
|
+
|
|
79
|
+
When you click `+` or `×`, the calculator needs to "freeze" the current number so you can type the next one. This brings up an interesting challenge in reactivity.
|
|
80
|
+
|
|
81
|
+
If we just saved a reference to `/c/display`, our stored value would keep changing as the user types. We need a snapshot.
|
|
82
|
+
|
|
83
|
+
Excel solves this with the `INDIRECT` function. JPRX does the same:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
{ button: {
|
|
87
|
+
class: "btn btn-operator",
|
|
88
|
+
onclick: =set(/c, {
|
|
89
|
+
prev: indirect(/c/display), // Capture the value right NOW
|
|
90
|
+
expr: concat(/c/display, " +"),
|
|
91
|
+
op: "+",
|
|
92
|
+
waiting: true
|
|
93
|
+
}),
|
|
94
|
+
children: ["+"]
|
|
95
|
+
} }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`indirect(/c/display)` reads the path and returns its **value** at that exact moment. It essentially dereferences the pointer.
|
|
99
|
+
|
|
100
|
+
## The `calc()` Helper
|
|
101
|
+
|
|
102
|
+
Evaluating the math is the final piece. We need a way to say: *"Take the previous value, apply the operator to the current display value, and give me the result."*
|
|
103
|
+
|
|
104
|
+
We use the `calc()` helper for this. To make it readable, `calc` supports a convenient shorthand: `$('path')`.
|
|
105
|
+
|
|
106
|
+
> **Note:** `$('/path')` is just syntactic sugar for `indirect('/path')` inside calculation strings. It tells the parser to fetch the value before doing the math.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
{ button: {
|
|
110
|
+
class: "btn btn-equals",
|
|
111
|
+
onclick: =set(/c, {
|
|
112
|
+
display: if(eq(/c/op, ""),
|
|
113
|
+
/c/display, // No op? Do nothing.
|
|
114
|
+
calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))
|
|
115
|
+
),
|
|
116
|
+
expr: concat(/c/expr, " ", /c/display, " ="),
|
|
117
|
+
prev: "",
|
|
118
|
+
op: "",
|
|
119
|
+
waiting: true
|
|
120
|
+
}),
|
|
121
|
+
children: ["="]
|
|
122
|
+
} }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If `prev` is 8, `op` is `*`, and `display` is 5, `calc` receives the string `"8 * 5"` and evaluates it safely.
|
|
126
|
+
|
|
127
|
+
## The Complete cDOMC Source
|
|
128
|
+
|
|
129
|
+
Here is the full source code for the calculator in `.cdomc` format:
|
|
130
|
+
|
|
131
|
+
> **Note:** cDOMC (compressed cDOM) supports comments and does not require quoting property names, making it more readable and maintainable than strict JSON.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// Pure cDOM/JPRX Calculator - No custom JavaScript helpers needed!
|
|
135
|
+
// Uses only core JPRX helpers:
|
|
136
|
+
// - state, set: state management
|
|
137
|
+
// - if, eq: conditionals
|
|
138
|
+
// - concat, contains: string operations
|
|
139
|
+
// - negate, toPercent: math operations
|
|
140
|
+
// - calc with $(): expression evaluation
|
|
141
|
+
{
|
|
142
|
+
div: {
|
|
143
|
+
class: "calculator",
|
|
144
|
+
onmount: =state({
|
|
145
|
+
display: "0",
|
|
146
|
+
expr: "",
|
|
147
|
+
prev: "",
|
|
148
|
+
op: "",
|
|
149
|
+
waiting: false
|
|
150
|
+
}, { name: "c", schema: "polymorphic", scope: $this }),
|
|
151
|
+
children: [
|
|
152
|
+
// Display area
|
|
153
|
+
{
|
|
154
|
+
div: {
|
|
155
|
+
class: "display",
|
|
156
|
+
children: [
|
|
157
|
+
{ div: { class: "expression", children: [=/c/expr] } },
|
|
158
|
+
{ div: { class: "result", children: [=/c/display] } }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
// Button grid
|
|
163
|
+
{
|
|
164
|
+
div: {
|
|
165
|
+
class: "buttons",
|
|
166
|
+
children: [
|
|
167
|
+
// Row 1: AC, ±, %, ÷
|
|
168
|
+
{ button: { class: "btn btn-clear", onclick: =set(/c, { display: "0", expr: "", prev: "", op: "", waiting: false }), children: ["AC"] } },
|
|
169
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: negate(/c/display), waiting: true, expr: "" }), children: ["±"] } },
|
|
170
|
+
{ button: { class: "btn btn-function", onclick: =set(/c, { display: toPercent(/c/display), waiting: true, expr: "" }), children: ["%"] } },
|
|
171
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ÷"), op: "/", waiting: true }), children: ["÷"] } },
|
|
172
|
+
|
|
173
|
+
// Row 2: 7, 8, 9, ×
|
|
174
|
+
{ button: { id: "7", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["7"] } },
|
|
175
|
+
{ button: { id: "8", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["8"] } },
|
|
176
|
+
{ button: { id: "9", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["9"] } },
|
|
177
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true }), children: ["×"] } },
|
|
178
|
+
|
|
179
|
+
// Row 3: 4, 5, 6, −
|
|
180
|
+
{ button: { id: "4", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["4"] } },
|
|
181
|
+
{ button: { id: "5", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["5"] } },
|
|
182
|
+
{ button: { id: "6", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["6"] } },
|
|
183
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true }), children: ["−"] } },
|
|
184
|
+
|
|
185
|
+
// Row 4: 1, 2, 3, +
|
|
186
|
+
{ button: { id: "1", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["1"] } },
|
|
187
|
+
{ button: { id: "2", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["2"] } },
|
|
188
|
+
{ button: { id: "3", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: ["3"] } },
|
|
189
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " +"), op: "+", waiting: true }), children: ["+"] } },
|
|
190
|
+
|
|
191
|
+
// Row 5: 0, ., =
|
|
192
|
+
{ button: { class: "btn btn-number btn-wide", onclick: =set(/c, { display: if(/c/waiting, "0", if(eq(/c/display, "0"), "0", concat(/c/display, "0"))), waiting: false }), children: ["0"] } },
|
|
193
|
+
{ button: { class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, "0.", if(contains(/c/display, "."), /c/display, concat(/c/display, "."))), waiting: false }), children: ["."] } },
|
|
194
|
+
{ button: { class: "btn btn-equals", onclick: =set(/c, { display: if(eq(/c/op, ""), /c/display, calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))), expr: concat(/c/expr, " ", /c/display, " ="), prev: "", op: "", waiting: true }), children: ["="] } }
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
// Branding
|
|
199
|
+
{
|
|
200
|
+
div: {
|
|
201
|
+
class: "branding",
|
|
202
|
+
children: [
|
|
203
|
+
{ span: { children: ["Built with ", { a: { href: "https://github.com/anywhichway/lightview", target: "_blank", children: ["Lightview"] } }, " cDOM • No custom JS!"] } }
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Loading cDOM via Hypermedia
|
|
213
|
+
|
|
214
|
+
Lightview supports loading cDOM content from external files using its hypermedia capability. Simply add a `src` attribute to any element, and Lightview will fetch and hydrate the cDOM content:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<!DOCTYPE html>
|
|
218
|
+
<html lang="en">
|
|
219
|
+
|
|
220
|
+
<head>
|
|
221
|
+
<meta charset="UTF-8">
|
|
222
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
223
|
+
<meta name="description"
|
|
224
|
+
content="A beautiful calculator built with Lightview cDOM and JPRX reactive expressions - no custom JavaScript!">
|
|
225
|
+
<title>Calculator | Lightview cDOM</title>
|
|
226
|
+
<link rel="stylesheet" href="calculator.css">
|
|
227
|
+
<!-- Load Lightview scripts -->
|
|
228
|
+
<script src="/lightview.js"></script> <!-- DOM as JSON and reactivity support -->
|
|
229
|
+
<script src="/lightview-x.js"></script> <!-- hypermedia support -->
|
|
230
|
+
<script src="/lightview-cdom.js"></script> <-- cDOM/JPRX support -->
|
|
231
|
+
</head>
|
|
232
|
+
|
|
233
|
+
<body>
|
|
234
|
+
<!-- The calculator cDOM is loaded via Lightview's hypermedia src attribute -->
|
|
235
|
+
<div id="app" src="./calculator.cdomc"></div>
|
|
236
|
+
</body>
|
|
237
|
+
|
|
238
|
+
</html>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The `src` attribute works like an HTML `<img>` or `<script>` tag—Lightview automatically fetches the `.cdomc` file, parses it, and renders the reactive content into the target element. This approach:
|
|
242
|
+
|
|
243
|
+
1. **Separates concerns:** Your HTML remains minimal while cDOM handles the component logic.
|
|
244
|
+
2. **Enables caching:** The browser can cache `.cdomc` files like any other static asset.
|
|
245
|
+
3. **Supports composition:** You can load different cDOM components into different parts of your page.
|
|
246
|
+
|
|
247
|
+
## Why Build This Way?
|
|
248
|
+
|
|
249
|
+
You might look at `concat("$('/c/prev') ...")` and ask: *Why not just write a JavaScript function?*
|
|
250
|
+
|
|
251
|
+
And for many cases, you should! Lightview allows standard JS handlers whenever you want them. But sticking to the declarative path has distinct advantages:
|
|
252
|
+
|
|
253
|
+
1. **Portability:** This entire UI—logic and all—is just JSON (well, JPRXC). It can be sent from a server, stored in a DB, or generated by an LLM.
|
|
254
|
+
2. **Sandboxing:** It executes in a controlled environment. The logic can't access `window`, make global fetch requests (unless allowed), or mess with the DOM outside its scope.
|
|
255
|
+
3. **Mental Model:** It forces you to think about the UI as a series of **state transformations** rather than imperative steps.
|
|
256
|
+
|
|
257
|
+
This calculator proves that "declarative" doesn't have to mean "dumb." With the right primitives—state, conditionals, and referencing—you can build rich, complex interactions without ever leaving the data structure.
|
|
258
|
+
|
|
259
|
+
> **A note on authorship:** This calculator wasn't hand-crafted by a human developer. It was generated by **Claude Opus** (Anthropic's LLM) after reading the [cDOM documentation](https://lightview.dev/docs/cdom) and the [JPRX npm package docs](https://www.npmjs.com/package/jprx). The fact that an AI could produce a fully functional, edge-case-handling calculator purely from documentation is exactly the point: cDOM and JPRX are formats that both humans and machines can reason about.
|
|
260
|
+
|
|
261
|
+
## The Bigger Picture
|
|
262
|
+
|
|
263
|
+
This article is the second in a series exploring how far we can push declarative, JSON-based UI development by both humans and LLMs.
|
|
264
|
+
|
|
265
|
+
**In [Part 1](https://medium.com/@anywhichway/building-reactive-uis-with-json-no-javascript-required-b7b1c4a45321)**, we introduced **cDOM**, a structural replacement for HTML and JSX. We focused on:
|
|
266
|
+
* Defining the UI hierarchy using standard JSON objects.
|
|
267
|
+
* Connecting to servers and LLMs with `fetch()` and `mount()`.
|
|
268
|
+
* Building a live dashboard that sorts and filters without server round-trips.
|
|
269
|
+
* *Key Concepts:* Reactive paths, operators (`++`, `--`), explosion operator (`...`), `$this`/`$event`, scoped state
|
|
270
|
+
* *Helpers:* `state()`, `set()`, `fetch()`, `mount()`, `move()`, `sum()`, `filter()`, `map()`, `sort()`, `push()`, `bind()`, `if()`, `formatDate()`, `formatNumber()`
|
|
271
|
+
|
|
272
|
+
**In Part 2 (this article)**, we tackled the missing piece: complex application logic. With **JPRX**, we proved that you don't need imperative JavaScript functions to handle:
|
|
273
|
+
* **State Machines:** `if()`, `eq()`, `set()`, `state()`
|
|
274
|
+
* **Context Awareness:** `$this`, `$event`
|
|
275
|
+
* **Data Capture:** `indirect()`, `$()`
|
|
276
|
+
* **Logic & Math:** `calc()`, `concat()`, `contains()`, `negate()`, `toPercent()`
|
|
277
|
+
|
|
278
|
+
Together, cDOM and JPRX offer a cohesive system: cDOM defines the *structure*, while JPRX defines the *behavior*. You get the reactivity of modern signals and the component model of a Virtual DOM, but in a portable format that requires no compilation, no build steps, and no context switching between languages. It's just data, all the way down.
|
|
279
|
+
|
|
280
|
+
### Try It Yourself
|
|
281
|
+
|
|
282
|
+
The complete calculator is available at:
|
|
283
|
+
|
|
284
|
+
**[Live Demo](https://lightview.dev/docs/calculator.html)**
|
|
285
|
+
|
|
286
|
+
**[Source Code](https://github.com/anywhichway/lightview/blob/main/docs/calculator.html)**
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
*Lightview is an open-source reactive UI library. Learn more at [github.com/anywhichway/lightview](https://github.com/anywhichway/lightview).*
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Building Reactive UIs with JSON — No JavaScript Required
|
|
2
|
+
|
|
3
|
+
*A declarative approach that lets both humans and LLMs create rich, interactive user interfaces using nothing but JSON.*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Power of JSON-Based UIs
|
|
8
|
+
|
|
9
|
+
What if you could build fully reactive, interactive user interfaces without writing a single line of JavaScript? What if the same format that's trivial for developers to write is equally easy for AI agents to generate?
|
|
10
|
+
|
|
11
|
+
This is the promise of cDOM (Computational DOM) and its expression language, JPRX (JSON Pointer Reactive eXpressions).
|
|
12
|
+
|
|
13
|
+
## Your First Reactive UI: A Counter in Pure JSON
|
|
14
|
+
|
|
15
|
+
Here's a complete, working interactive counter — no JavaScript code required:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"div": {
|
|
20
|
+
"onmount": "=state({ count: 0 }, { name: 'local', scope: $this })",
|
|
21
|
+
"children": [
|
|
22
|
+
{ "p": ["Count: ", "=local/count"] },
|
|
23
|
+
{ "button": { "onclick": "=local/count++", "children": ["+"] } }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's the entire application. Let's break down what's happening:
|
|
30
|
+
|
|
31
|
+
- `state({ count: 0 })` initializes reactive state scoped to this element
|
|
32
|
+
- `=local/count` is a live binding—the text updates automatically when count changes
|
|
33
|
+
- `=local/count++` increments the counter directly when clicked
|
|
34
|
+
- The UI reacts instantly in the browser, no server needed
|
|
35
|
+
|
|
36
|
+
This is the spreadsheet paradigm applied to user interfaces. Just like writing `=SUM(A1:A10)` in Excel, you define the relationships and the system handles the updates.
|
|
37
|
+
|
|
38
|
+
## Why This Matters for Humans
|
|
39
|
+
|
|
40
|
+
As a developer, you're freed from the boilerplate of event handlers, state management, and manual DOM updates. Instead of imperative code that says "when this happens, do these five things," you write declarative expressions that say "this value always equals that calculation."
|
|
41
|
+
|
|
42
|
+
Compare traditional JavaScript:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
let count = 0;
|
|
46
|
+
const countDisplay = document.getElementById('count');
|
|
47
|
+
const button = document.getElementById('increment');
|
|
48
|
+
button.addEventListener('click', () => {
|
|
49
|
+
count++;
|
|
50
|
+
countDisplay.textContent = `Count: ${count}`;
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
cDOM/JPRX:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"button": {
|
|
59
|
+
"onclick": "=/local/count++",
|
|
60
|
+
"children": ["Clicks:", "=/local/count"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why This Matters for LLMs
|
|
66
|
+
|
|
67
|
+
For AI agents, cDOM provides a safe, structured format for generating UIs:
|
|
68
|
+
|
|
69
|
+
- **Context-Free Grammar:** No closures, no callbacks, no complex JavaScript patterns. An LLM generates JPRX as easily as it generates natural language.
|
|
70
|
+
- **Safe by Design:** No access to eval, arbitrary code execution, or unrestricted DOM access. The application developer controls what's possible through a catalog of helper functions.
|
|
71
|
+
- **Declarative:** Less chance to get lost with slop generation in the freedom of the JavaScript language.
|
|
72
|
+
- **Streamable:** Components are self-describing and self-contained. An LLM can stream UI updates one piece at a time without regenerating entire pages.
|
|
73
|
+
|
|
74
|
+
## Connecting to Servers and LLMs
|
|
75
|
+
|
|
76
|
+
When you need server interaction, it's just another expression. Want to notify an LLM when a button is clicked?
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"button": {
|
|
81
|
+
"onclick": "=fetch('/api/notify', { method: 'POST', body: $event })",
|
|
82
|
+
"children": ["Notify LLM"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `=fetch` helper automatically:
|
|
88
|
+
- Sends a POST request to your endpoint
|
|
89
|
+
- Stringifies JSON bodies and sets the correct Content-Type
|
|
90
|
+
- Lets your backend (or LLM) process the event and respond
|
|
91
|
+
|
|
92
|
+
This is event registration, not constant chatter. The LLM only hears about the interactions you choose to wire up — no need to notify it of every mouse move or keystroke.
|
|
93
|
+
|
|
94
|
+
## LLMs That Push UI Updates
|
|
95
|
+
|
|
96
|
+
Here's where things get powerful. What if the LLM wants to add a new component to the page?
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"button": {
|
|
101
|
+
"onclick": "=mount('/api/get-widget')",
|
|
102
|
+
"children": ["Load Widget"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When clicked, `mount` fetches JSON from your endpoint and injects it as a live, reactive component. The LLM can respond with:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"div": {
|
|
112
|
+
"id": "weather-widget",
|
|
113
|
+
"onmount": "=move('#dashboard-sidebar', 'afterbegin')",
|
|
114
|
+
"children": ["Sunny, 75°F"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The widget automatically:
|
|
120
|
+
- Mounts safely to the document
|
|
121
|
+
- Moves itself to the specified location (`#dashboard-sidebar`)
|
|
122
|
+
- Replaces any existing widget with the same ID (idempotent updates)
|
|
123
|
+
|
|
124
|
+
The LLM doesn't need to understand your entire page structure — it just pushes components that know where they belong.
|
|
125
|
+
|
|
126
|
+
## Three Levels of Capability
|
|
127
|
+
|
|
128
|
+
| Level | Use Case | Example Helper | Server Involved? |
|
|
129
|
+
|-------|----------|----------------|------------------|
|
|
130
|
+
| 1 | Client-only reactivity | `=state`, `=++`, `=sum()` | No |
|
|
131
|
+
| 2 | Notify server of actions | `=fetch` | Yes |
|
|
132
|
+
| 3 | Server pushes new UI | `=mount`, `=move` | Yes |
|
|
133
|
+
|
|
134
|
+
This layered approach means you can build:
|
|
135
|
+
- Fully offline apps that handle all interactions locally (Level 1)
|
|
136
|
+
- Hybrid apps where servers are notified selectively (Level 2)
|
|
137
|
+
- Agent-driven apps where LLMs control the UI in real-time (Level 3)
|
|
138
|
+
|
|
139
|
+
All using the same JSON format.
|
|
140
|
+
|
|
141
|
+
## JPRX: Excel Formulas for UI
|
|
142
|
+
|
|
143
|
+
JPRX expressions are designed to feel familiar if you've ever used spreadsheets:
|
|
144
|
+
|
|
145
|
+
- **Reactive paths:** `=app/user/name` automatically updates when the data changes
|
|
146
|
+
- **Operators:** `=++count`, `=count--`, `=!!enabled`
|
|
147
|
+
- **Helper functions:** Over 100 functions covering math, strings, arrays, dates, and more
|
|
148
|
+
- `=sum(/cart/items...price)` - Sum all item prices
|
|
149
|
+
- `=filter(/users, age > 18)` - Filter array by condition
|
|
150
|
+
- `=formatDate(/order/date, 'MM/DD/YYYY')` - Format dates
|
|
151
|
+
- `=if(total > 100, 'Bulk', 'Regular')` - Conditional logic
|
|
152
|
+
- **State mutations:** `=set(/app/user/name, 'John')`, `=push(/cart/items, $newItem)`
|
|
153
|
+
- **Relative paths:** Use `../` to navigate up context hierarchies
|
|
154
|
+
|
|
155
|
+
If you can write `=SUM(A1:A10)` in Excel, you can write reactive UIs with JPRX.
|
|
156
|
+
|
|
157
|
+
## A Real-World Example: Live Dashboard
|
|
158
|
+
|
|
159
|
+
Imagine a user asks an LLM:
|
|
160
|
+
|
|
161
|
+
> "Show me my sales performance for each region in Q4, highlight anything below target, and let me sort the results."
|
|
162
|
+
|
|
163
|
+
The LLM responds not with paragraphs of text, but with a live, interactive dashboard:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"div": {
|
|
168
|
+
"id": "sales-dashboard",
|
|
169
|
+
"onmount": "=fetch('/api/sales/q4').then(data => state({ data, sort: 'name' }, 'sales'))",
|
|
170
|
+
"children": [
|
|
171
|
+
{ "h2": ["Q4 Sales Performance"] },
|
|
172
|
+
{
|
|
173
|
+
"table": {
|
|
174
|
+
"children": [
|
|
175
|
+
{ "thead": [
|
|
176
|
+
{ "tr": [
|
|
177
|
+
{ "th": { "children": ["Region"], "onclick": "=set('sales/sort', 'name')" } },
|
|
178
|
+
{ "th": { "children": ["Revenue"], "onclick": "=set('sales/sort', 'revenue')" } },
|
|
179
|
+
{ "th": { "children": ["Target"], "onclick": "=set('sales/sort', 'target')" } },
|
|
180
|
+
{ "th": { "children": ["Status"], "onclick": "=set('sales/sort', 'status')" } }
|
|
181
|
+
]}
|
|
182
|
+
]},
|
|
183
|
+
{ "tbody": {
|
|
184
|
+
"children": "=map(sort(sales.data, (a, b) => a[sales.sort] > b[sales.sort] ? 1 : -1), region => ({ tr: { class: region.revenue < region.target ? 'below-target' : '', children: [ { td: [region.name] }, { td: ['$', formatNumber(region.revenue)] }, { td: ['$', formatNumber(region.target)] }, { td: [region.revenue >= region.target ? '✓' : '⚠'] } ] } }))"
|
|
185
|
+
}}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This entire dashboard is:
|
|
195
|
+
- **Reactive:** All calculations update automatically
|
|
196
|
+
- **Self-contained:** No separate JavaScript files
|
|
197
|
+
- **Safe:** Uses only approved helper functions
|
|
198
|
+
- **Readable:** Both humans and LLMs can understand it
|
|
199
|
+
|
|
200
|
+
When the user sorts, no round trip to the server is required.
|
|
201
|
+
|
|
202
|
+
## The Vision: Agent-First Applications
|
|
203
|
+
|
|
204
|
+
The next wave of applications won't be chatbots with UI bolted on. They'll be intelligent systems where conversation and application merge seamlessly.
|
|
205
|
+
|
|
206
|
+
A user shouldn't have to parse walls of text to understand complex data. An agent should be able to respond with:
|
|
207
|
+
- A live chart that filters
|
|
208
|
+
- A table that sorts
|
|
209
|
+
- A summary that recalculates
|
|
210
|
+
- All instantly reactive, no page reloads
|
|
211
|
+
|
|
212
|
+
cDOM makes this possible by giving both humans and LLMs a shared language for describing not just what the UI looks like, but how it behaves.
|
|
213
|
+
|
|
214
|
+
## Getting Started
|
|
215
|
+
|
|
216
|
+
cDOM and JPRX are part of Lightview, a reactive UI library designed for both human developers and AI agents.
|
|
217
|
+
|
|
218
|
+
- **Documentation:** [lightview.dev](https://lightview.dev)
|
|
219
|
+
- **GitHub:** [github.com/anywhichway/lightview](https://github.com/anywhichway/lightview)
|
|
220
|
+
- **npm:**
|
|
221
|
+
- Full library: `lightview`
|
|
222
|
+
- Standalone parser: `jprx`
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
*The Spreadsheet Moment for UI*
|
|
227
|
+
|
|
228
|
+
Spreadsheets democratized computation. You didn't need to be a programmer to define that `C3 = A1 + B2` and watch it update automatically.
|
|
229
|
+
|
|
230
|
+
cDOM aims to be that moment for user interfaces. A format where anyone — human or AI — can declare:
|
|
231
|
+
- This text shows the count
|
|
232
|
+
- This button increments it
|
|
233
|
+
- This chart sums the sales
|
|
234
|
+
- This table filters by region
|
|
235
|
+
|
|
236
|
+
And the system handles the rest. No JavaScript required.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Pure cDOM/JPRX Calculator - No custom JavaScript helpers needed!
|
|
2
|
+
// Uses only core JPRX helpers:
|
|
3
|
+
// - state, =: state management
|
|
4
|
+
// - if, eq: conditionals
|
|
5
|
+
// - concat, contains: string operations
|
|
6
|
+
// - negate, toPercent: math operations
|
|
7
|
+
// - calc with $(): expression evaluation
|
|
8
|
+
// - XPath (#../@attr): DOM navigation for DRY button definitions
|
|
9
|
+
{
|
|
10
|
+
div: {
|
|
11
|
+
class: "calculator",
|
|
12
|
+
onmount: =state({
|
|
13
|
+
display: "0",
|
|
14
|
+
expr: "",
|
|
15
|
+
prev: "",
|
|
16
|
+
op: "",
|
|
17
|
+
waiting: false
|
|
18
|
+
}, { name: "c", schema: "polymorphic", scope: $this }),
|
|
19
|
+
children: [
|
|
20
|
+
// Display area
|
|
21
|
+
{
|
|
22
|
+
div: {
|
|
23
|
+
class: "display",
|
|
24
|
+
children: [
|
|
25
|
+
{ div: { class: "expression", children: [=/c/expr] } },
|
|
26
|
+
{ div: { class: "result", children: [=/c/display] } }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
// Button grid
|
|
31
|
+
{
|
|
32
|
+
div: {
|
|
33
|
+
class: "buttons",
|
|
34
|
+
children: [
|
|
35
|
+
// Row 1: AC, ±, %, ÷
|
|
36
|
+
{ button: { class: "btn btn-clear", onclick: =/c = { display: "0", expr: "", prev: "", op: "", waiting: false }, children: ["AC"] } },
|
|
37
|
+
{ button: { class: "btn btn-function", onclick: =/c = { display: negate(/c/display), waiting: true, expr: "" }, children: ["±"] } },
|
|
38
|
+
{ button: { class: "btn btn-function", onclick: =/c = { display: toPercent(/c/display), waiting: true, expr: "" }, children: ["%"] } },
|
|
39
|
+
{ button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), expr: concat(/c/display, " ÷"), op: "/", waiting: true }, children: ["÷"] } },
|
|
40
|
+
|
|
41
|
+
// Row 2: 7, 8, 9, ×
|
|
42
|
+
{ button: { id: "7", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
43
|
+
{ button: { id: "8", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
44
|
+
{ button: { id: "9", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
45
|
+
{ button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), expr: concat(/c/display, " ×"), op: "*", waiting: true }, children: ["×"] } },
|
|
46
|
+
|
|
47
|
+
// Row 3: 4, 5, 6, −
|
|
48
|
+
{ button: { id: "4", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
49
|
+
{ button: { id: "5", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
50
|
+
{ button: { id: "6", class: "btn btn-number", onclick: =/c = { display: if(/c/waiting, $this.id, if(/c/display==0, $this.id, concat(/c/display, $this.id))), waiting: false }, children: [#../@id] } },
|
|
51
|
+
{ button: { class: "btn btn-operator", onclick: =/c = { prev: indirect(/c/display), expr: concat(/c/display, " −"), op: "-", waiting: true }, children: ["−"] } },
|
|
52
|
+
|
|
53
|
+
// Row 4: 1, 2, 3, +, use set and eq just to demonstrate equivalence with =
|
|
54
|
+
{ button: { id: "1", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } },
|
|
55
|
+
{ button: { id: "2", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } },
|
|
56
|
+
{ button: { id: "3", class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, $this.id, if(eq(/c/display, "0"), $this.id, concat(/c/display, $this.id))), waiting: false }), children: [#../@id] } },
|
|
57
|
+
{ button: { class: "btn btn-operator", onclick: =set(/c, { prev: indirect(/c/display), expr: concat(/c/display, " +"), op: "+", waiting: true }), children: ["+"] } },
|
|
58
|
+
|
|
59
|
+
// Row 5: 0, ., =
|
|
60
|
+
{ button: { class: "btn btn-number btn-wide", onclick: =set(/c, { display: if(/c/waiting, "0", if(eq(/c/display, "0"), "0", concat(/c/display, "0"))), waiting: false }), children: ["0"] } },
|
|
61
|
+
{ button: { class: "btn btn-number", onclick: =set(/c, { display: if(/c/waiting, "0.", if(contains(/c/display, "."), /c/display, concat(/c/display, "."))), waiting: false }), children: ["."] } },
|
|
62
|
+
{ button: { class: "btn btn-equals", onclick: =set(/c, { display: if(eq(/c/op, ""), /c/display, calc(concat("$('/c/prev') ", /c/op, " $('/c/display')"))), expr: concat(/c/expr, " ", /c/display, " ="), prev: "", op: "", waiting: true }), children: ["="] } }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
// Branding
|
|
67
|
+
{
|
|
68
|
+
div: {
|
|
69
|
+
class: "branding",
|
|
70
|
+
children: [
|
|
71
|
+
{ span: { children: ["Built with ", { a: { href: "https://github.com/anywhichway/lightview", target: "_blank", children: ["Lightview"] } }, " cDOM • No custom JS!"] } }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|