lightview 2.3.5 → 2.3.7
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 +186 -0
- package/1769196538097-package.json +43 -0
- package/_headers +5 -0
- package/_routes.json +7 -0
- package/build-bundles.mjs +10 -0
- package/build_tmp/lightview-router.js +185 -0
- package/build_tmp/lightview-x.js +1608 -0
- package/build_tmp/lightview.js +932 -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/docs/cdom-nav.html +37 -31
- package/docs/cdom-xpath.html +160 -0
- package/docs/cdom.html +71 -12
- package/functions/_middleware.js +20 -3
- package/jprx/helpers/dom.js +69 -0
- package/jprx/helpers/logic.js +9 -3
- package/jprx/index.js +3 -1
- package/jprx/package.json +1 -1
- package/jprx/parser.js +363 -82
- package/lightview-all.js +476 -77
- package/lightview-cdom.js +474 -74
- package/lightview-x.js +4 -3
- package/lightview.js +10 -0
- package/package.json +2 -2
- package/src/lightview-cdom.js +185 -11
- package/src/lightview-x.js +5 -2
- package/src/lightview.js +16 -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,283 @@
|
|
|
1
|
+
# Building a Fully Functional Calculator with Zero Custom JavaScript
|
|
2
|
+
|
|
3
|
+
*How a JSON or JSON-like language enables the next generation of safe human and AI-generated UIs*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
In [The Future of AI-Generated UI](https://hackernoon.com/the-future-of-ai-generated-ui-why-i-built-cdom-and-jprx), I made the case that raw JavaScript is a security nightmare for AI-generated code. I argued that we need declarative, sandboxed formats like **cDOM** (structure) and **JPRX** (behavior) if we ever want to trust an LLM to build our interfaces.
|
|
8
|
+
|
|
9
|
+
But let's be real: reactive counters and to-do lists are easy. Any framework looks elegant when the logic fits on a napkin. To prove a JSON-based approach actually holds up in production, you need a problem with messy state, edge cases, and distinct modes of operation.
|
|
10
|
+
|
|
11
|
+
You need a calculator.
|
|
12
|
+
|
|
13
|
+
Calculators are inherently tricky:
|
|
14
|
+
- **Input Modes**: Are we typing a fresh number or appending to an existing one?
|
|
15
|
+
- **Chaining**: What happens when you hit `+` then `-` then `*` without hitting equals?
|
|
16
|
+
- **DRY Logic**: How do we avoid writing 10 separate handlers for buttons 0-9?
|
|
17
|
+
|
|
18
|
+
So I built (well actually Claude Opus built) a fully functional, iOS-style calculator using **zero custom JavaScript functions** - just declarative cDOM and JPRX expressions.
|
|
19
|
+
|
|
20
|
+
## The Result
|
|
21
|
+
|
|
22
|
+
A fully functional, iOS-style interface is handled purely by the expression language. No hidden `script` tags, no helper functions.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## The State Machine
|
|
27
|
+
|
|
28
|
+
A calculator feels stateless, but it's actually a strict state machine. You're never just "typing a number"; you're either entering the first operand, waiting for an operator, or typing the second operand.
|
|
29
|
+
|
|
30
|
+
We model this brain directly in the component's mount event:
|
|
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
|
+
The classic DRY (Don't Repeat Yourself) challenge: I have 10 number buttons. Do I write 10 handlers? Do I write a loop? In React or Vue, you'd probably map over an array.
|
|
47
|
+
|
|
48
|
+
With JPRX, the DOM *is* the data key.
|
|
49
|
+
|
|
50
|
+
By giving each button an `id` (e.g., `id: "7"`), we write a **single logic expression** that adapts to whichever element triggered it. We just 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 what is happening:
|
|
71
|
+
1. **Waiting for input?** (e.g., just hit `+`) → Replace the display with the button's ID.
|
|
72
|
+
2. **Displaying "0"?** → Replace it (avoids "07").
|
|
73
|
+
3. **Otherwise:** → Append the button's ID.
|
|
74
|
+
|
|
75
|
+
This works identically for every number button. No loops, no external helper functions.
|
|
76
|
+
|
|
77
|
+
## Operators and "Indirect" Values
|
|
78
|
+
|
|
79
|
+
This is where it gets tricky. When you click `+`, you can't just link `prev` to `display`. If you did, `prev` would update every time you selected a new digit for the *second* number, breaking the math.
|
|
80
|
+
|
|
81
|
+
We need a snapshot of the value *at that exact moment*.
|
|
82
|
+
|
|
83
|
+
Excel solves this with `INDIRECT`, effectively dereferencing a cell. JPRX borrows the same concept:
|
|
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
|
+
Finally, the math. We need to say: *"Take the snapshot we stored, apply the current operator, and combine it with what's on screen now."*
|
|
103
|
+
|
|
104
|
+
This is the job of `calc()`. To keep the syntax clean, we use `$('path')` as a shorthand for capturing values right before evaluation.
|
|
105
|
+
|
|
106
|
+
> **Note:** `$('/path')` is just syntactic sugar for `indirect('/path')`.
|
|
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. Lightview also supports parsing cDOM from regular JSON with quotes, so you can use it with any JSON parser.
|
|
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 in the world wouldn't you just write `parseFloat(prev) + parseFloat(curr)`?*
|
|
250
|
+
|
|
251
|
+
If you are a human writing code for yourself? You probably should. Lightview supports standard JS handlers for exactly that reason.
|
|
252
|
+
|
|
253
|
+
But if you are building infrastructure for **AI Agents**, the calculus changes. Sticking to a declarative, JSON-based path offers things raw code can't:
|
|
254
|
+
|
|
255
|
+
1. **Sandboxing:** It executes in a controlled environment. The logic can't access `window`, make global fetch requests, or execute arbitrary secondary code. This makes it safe to "hot swap" UI logic generated by an LLM in real-time.
|
|
256
|
+
2. **Portability:** This entire UI—logic and all—is just data. It can be sent from a server, stored in a database, or streamed from an AI model.
|
|
257
|
+
3. **Mental Model:** It forces a clear separation between state transformations and view structure, which is exactly how LLMs reason best.
|
|
258
|
+
|
|
259
|
+
This calculator proves that "declarative" doesn't have to mean "dumb." With the right primitives—state, conditionals, and path-based referencing—you can build rich, complex interactions without ever leaving the data structure.
|
|
260
|
+
|
|
261
|
+
> **A Note on Authorship:** This calculator wasn't hand-crafted. It was generated by **Claude Opus** after reading the [cDOM documentation](https://lightview.dev/docs/cdom). The fact that an AI could produce a robust, edge-case-handling calculator purely from documentation proves the point: cDOM and JPRX aren't just new syntax. They are a protocol for human-machine collaboration.
|
|
262
|
+
|
|
263
|
+
## The Bigger Picture
|
|
264
|
+
|
|
265
|
+
This series isn't just about a new library. It's about finding the right abstraction layer for the AI age.
|
|
266
|
+
|
|
267
|
+
**In [Part 1](https://hackernoon.com/the-future-of-ai-generated-ui-why-i-built-cdom-and-jprx)**, we looked at the security risks of letting LLMs write raw scripts and introduced the "Data as UI" philosophy.
|
|
268
|
+
|
|
269
|
+
**In this article**, we proved that "Data as UI" doesn't mean "dumb UI." We handled state, context, data snapshots, and math without executing a single line of imperative code.
|
|
270
|
+
|
|
271
|
+
cDOM defines structure. JPRX defines behavior. It’s reactivity without the compilation and UI without the security risks.
|
|
272
|
+
|
|
273
|
+
### Try It Yourself
|
|
274
|
+
|
|
275
|
+
The complete calculator is available at:
|
|
276
|
+
|
|
277
|
+
**[Live Demo](https://lightview.dev/docs/calculator.html)**
|
|
278
|
+
|
|
279
|
+
**[Source Code](https://github.com/anywhichway/lightview/blob/main/docs/calculator.html)**
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
*Lightview is an open-source reactive UI library. Learn more at [github.com/anywhichway/lightview](https://github.com/anywhichway/lightview).*
|
|
@@ -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).*
|