lightview 2.2.2 → 2.3.4
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/cDOMIntro.md +279 -0
- package/docs/about.html +15 -12
- package/docs/api/computed.html +1 -1
- package/docs/api/effects.html +1 -1
- package/docs/api/elements.html +56 -25
- package/docs/api/enhance.html +1 -1
- package/docs/api/hypermedia.html +1 -1
- package/docs/api/index.html +1 -1
- package/docs/api/nav.html +28 -3
- package/docs/api/signals.html +1 -1
- package/docs/api/state.html +283 -85
- package/docs/assets/js/examplify.js +2 -1
- package/docs/cdom-nav.html +3 -2
- package/docs/cdom.html +383 -114
- package/jprx/README.md +112 -71
- package/jprx/helpers/state.js +21 -0
- package/jprx/package.json +1 -1
- package/jprx/parser.js +136 -86
- package/jprx/specs/expressions.json +71 -0
- package/jprx/specs/helpers.json +150 -0
- package/lightview-all.js +618 -431
- package/lightview-cdom.js +311 -605
- package/lightview-router.js +6 -0
- package/lightview-x.js +226 -54
- package/lightview.js +351 -42
- package/package.json +2 -1
- package/src/lightview-cdom.js +211 -315
- package/src/lightview-router.js +10 -0
- package/src/lightview-x.js +121 -1
- package/src/lightview.js +88 -16
- package/src/reactivity/signal.js +73 -29
- package/src/reactivity/state.js +84 -21
- package/tests/cdom/fixtures/helpers.cdomc +24 -24
- package/tests/cdom/helpers.test.js +28 -28
- package/tests/cdom/parser.test.js +39 -114
- package/tests/cdom/reactivity.test.js +32 -29
- package/tests/jprx/spec.test.js +99 -0
- package/tests/cdom/loader.test.js +0 -125
package/docs/cdom.html
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
<script>
|
|
55
55
|
globalThis.switchCDOMTab = (tabId) => {
|
|
56
|
-
const tabs = ['jprx', 'jprxc', 'operators'];
|
|
56
|
+
const tabs = ['jprx', 'jprxc', 'operators', 'embedded'];
|
|
57
57
|
tabs.forEach(t => {
|
|
58
58
|
const btn = document.getElementById(`tab-btn-${t}`);
|
|
59
59
|
const pane = document.getElementById(`pane-${t}`);
|
|
@@ -77,6 +77,22 @@
|
|
|
77
77
|
A declarative, expression-based way to build reactive UIs.
|
|
78
78
|
</p>
|
|
79
79
|
|
|
80
|
+
<div class="experimental-notice"
|
|
81
|
+
style="margin: 1.5rem 0; padding: 1.25rem; border-radius: var(--site-radius); background: var(--site-accent-light); border: 1px solid var(--site-warning); color: var(--site-text);">
|
|
82
|
+
<div style="display: flex; gap: 0.75rem; align-items: flex-start;">
|
|
83
|
+
<span style="font-size: 1.5rem;">⚠️</span>
|
|
84
|
+
<div>
|
|
85
|
+
<strong style="display: block; margin-bottom: 0.25rem; font-size: 1.1rem;">Experimental
|
|
86
|
+
Feature</strong>
|
|
87
|
+
<p style="margin: 0; font-size: 0.95rem; opacity: 0.9;">
|
|
88
|
+
cDOM and JPRX are currently in an <strong>experimental</strong> phase. The expression syntax,
|
|
89
|
+
helper functions, and integration patterns are subject to change as we continue to evolve the
|
|
90
|
+
library. Use with caution in production environments.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
80
96
|
<!-- ===== OVERVIEW ===== -->
|
|
81
97
|
<h2 id="overview">Overview</h2>
|
|
82
98
|
<p>
|
|
@@ -92,44 +108,57 @@
|
|
|
92
108
|
<p>
|
|
93
109
|
cDOM uses <strong>JPRX (JSON Pointer Reactive eXpressions)</strong> as its expression language. JPRX
|
|
94
110
|
extends <a href="https://www.rfc-editor.org/rfc/rfc6901" target="_blank">JSON Pointer (RFC 6901)</a>
|
|
95
|
-
with reactivity, relative paths, and helper functions.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
If you wish to explore the implementation further, you can visit the <a
|
|
99
|
-
href="https://github.com/anywhichway/lightview" target="_blank">GitHub repository</a>, browse the <a
|
|
100
|
-
href="https://github.com/anywhichway/lightview/tree/main/cdom" target="_blank">cdom directory</a>, and
|
|
101
|
-
check out the <a href="https://github.com/anywhichway/lightview/tree/main/tests/cdom" target="_blank">cdom
|
|
102
|
-
unit tests</a>.
|
|
111
|
+
with reactivity, relative paths, and helper functions. It also provides deep integration with
|
|
112
|
+
<a href="https://json-schema.org/" target="_blank">JSON Schema</a> (Standard Draft 7+) for
|
|
113
|
+
industrial-strength data validation and automatic type coercion.
|
|
103
114
|
</p>
|
|
104
115
|
|
|
105
116
|
<!-- ===== SIMPLE EXAMPLE ===== -->
|
|
106
117
|
<h2 id="simple-example">Simple Example</h2>
|
|
107
118
|
<p>Here's a simple counter in cDOM. Notice how the UI is purely declarative — no JavaScript logic:</p>
|
|
108
|
-
<div
|
|
109
|
-
<pre><
|
|
119
|
+
<div id="simple-counter-demo">
|
|
120
|
+
<pre><script>
|
|
121
|
+
examplify(document.currentScript.nextElementSibling, {
|
|
122
|
+
at: document.currentScript.parentElement,
|
|
123
|
+
scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
|
|
124
|
+
type: 'module',
|
|
125
|
+
height: '200px',
|
|
126
|
+
autoRun: true,
|
|
127
|
+
controls: false
|
|
128
|
+
});
|
|
129
|
+
</script><code>await import('/lightview-cdom.js');
|
|
130
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
131
|
+
const { $ } = Lightview;
|
|
132
|
+
|
|
133
|
+
const cdom = `{
|
|
110
134
|
div: {
|
|
111
|
-
|
|
135
|
+
onmount: =state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this }),
|
|
112
136
|
children: [
|
|
113
137
|
{ h2: "Counter" },
|
|
114
|
-
{ p: ["Count: ",
|
|
115
|
-
{ button: { onclick:
|
|
116
|
-
{ button: { onclick:
|
|
138
|
+
{ p: ["Count: ", =/local/count] },
|
|
139
|
+
{ button: { onclick: =++/local/count, children: ["+"] } },
|
|
140
|
+
{ button: { onclick: =--/local/count, children: ["-"] } }
|
|
117
141
|
]
|
|
118
142
|
}
|
|
119
|
-
}
|
|
143
|
+
}`;
|
|
144
|
+
|
|
145
|
+
$('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
|
|
120
146
|
</div>
|
|
121
147
|
<p>
|
|
122
148
|
This defines a counter with:
|
|
123
149
|
</p>
|
|
124
150
|
<ul>
|
|
125
|
-
<li><code>
|
|
126
|
-
<li><code
|
|
127
|
-
<li><code
|
|
128
|
-
<li><code
|
|
151
|
+
<li><code>onmount</code> — A Lightview lifecycle hook where we initialize state.</li>
|
|
152
|
+
<li><code>=state(...)</code> — An initializer that creates local reactive state.</li>
|
|
153
|
+
<li><code>scope: $this</code> — Explicitly attaches the state to the current element.</li>
|
|
154
|
+
<li><code>=/local/count</code> — A path that reactively displays the count value.</li>
|
|
129
155
|
</ul>
|
|
130
156
|
<p>
|
|
131
157
|
The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required.
|
|
132
158
|
</p>
|
|
159
|
+
<p>
|
|
160
|
+
The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required.
|
|
161
|
+
</p>
|
|
133
162
|
|
|
134
163
|
<!-- ===== ADVANTAGES ===== -->
|
|
135
164
|
<h2 id="advantages">Advantages</h2>
|
|
@@ -162,7 +191,7 @@
|
|
|
162
191
|
|
|
163
192
|
<h3 id="JPRX-delimiters">Delimiters</h3>
|
|
164
193
|
<p>
|
|
165
|
-
JPRX expressions begin with a <code
|
|
194
|
+
JPRX expressions begin with a <code>=</code> delimiter:
|
|
166
195
|
</p>
|
|
167
196
|
<table class="api-table">
|
|
168
197
|
<thead>
|
|
@@ -174,20 +203,40 @@
|
|
|
174
203
|
</thead>
|
|
175
204
|
<tbody>
|
|
176
205
|
<tr>
|
|
177
|
-
<td><code
|
|
206
|
+
<td><code>=/</code></td>
|
|
178
207
|
<td>Access a path in the global registry</td>
|
|
179
|
-
<td><code
|
|
208
|
+
<td><code>=/users/0/name</code></td>
|
|
180
209
|
</tr>
|
|
181
210
|
<tr>
|
|
182
|
-
<td><code
|
|
211
|
+
<td><code>=function(</code></td>
|
|
183
212
|
<td>Call a helper function</td>
|
|
184
|
-
<td><code
|
|
213
|
+
<td><code>=sum(/items...price)</code></td>
|
|
185
214
|
</tr>
|
|
186
215
|
</tbody>
|
|
187
216
|
</table>
|
|
188
217
|
<p>
|
|
189
|
-
Once inside a JPRX expression, paths follow <strong>JSON Pointer</strong> syntax. The <code
|
|
190
|
-
is only needed at the start of the expression.
|
|
218
|
+
Once inside a JPRX expression, paths follow <strong>JSON Pointer</strong> syntax. The <code>=</code>
|
|
219
|
+
is only needed at the start of the expression for paths or function names.
|
|
220
|
+
</p>
|
|
221
|
+
|
|
222
|
+
<h3 id="JPRX-escaping">Escaping the <code>=</code> Delimiter</h3>
|
|
223
|
+
<p>
|
|
224
|
+
When using <strong>oDOM</strong> or <strong>vDOM</strong> (Object DOM), any string value starting with
|
|
225
|
+
<code>=</code> is interpreted as a JPRX expression. If you need a literal string that begins with
|
|
226
|
+
an equals sign (e.g., a mathematical equation or status message), you can escape it by prefixing
|
|
227
|
+
with a single quote:
|
|
228
|
+
</p>
|
|
229
|
+
<div class="code-block" style="margin-bottom: 1rem;">
|
|
230
|
+
<pre><code>// Interpreted as JPRX expression:
|
|
231
|
+
{ "p": "=/user/name" } // Resolves the path /user/name
|
|
232
|
+
|
|
233
|
+
// Escaped to produce a literal string:
|
|
234
|
+
{ "p": "'=E=mc²" } // Renders as "=E=mc²"
|
|
235
|
+
{ "p": "'=42" } // Renders as "=42"</code></pre>
|
|
236
|
+
</div>
|
|
237
|
+
<p>
|
|
238
|
+
The single-quote escape (<code>'=</code>) at the start of a string tells the parser to treat
|
|
239
|
+
the rest of the string (including the <code>=</code>) as literal content.
|
|
191
240
|
</p>
|
|
192
241
|
|
|
193
242
|
<h3 id="JPRX-anatomy">Anatomy of a Path</h3>
|
|
@@ -236,7 +285,7 @@
|
|
|
236
285
|
Paths can contain function calls to transform data:
|
|
237
286
|
</p>
|
|
238
287
|
<div class="code-block">
|
|
239
|
-
<pre><code
|
|
288
|
+
<pre><code>=currency(sum(map(filter(/orders, eq(_/status, 'paid')), _/total)...))</code></pre>
|
|
240
289
|
</div>
|
|
241
290
|
<ul>
|
|
242
291
|
<li><strong>/orders</strong>: Access the <code>orders</code> collection (JSON Pointer)</li>
|
|
@@ -244,7 +293,7 @@
|
|
|
244
293
|
<li><strong>map(..., _/total)</strong>: Extract the <code>total</code> from each order</li>
|
|
245
294
|
<li><strong>...</strong>: Explode the array into individual arguments</li>
|
|
246
295
|
<li><strong>sum(...)</strong>: Add up all the totals</li>
|
|
247
|
-
<li><strong
|
|
296
|
+
<li><strong>=currency(...)</strong>: Format as currency string</li>
|
|
248
297
|
</ul>
|
|
249
298
|
|
|
250
299
|
<h3 id="JPRX-placeholders">Placeholders</h3>
|
|
@@ -260,12 +309,12 @@
|
|
|
260
309
|
<tr>
|
|
261
310
|
<td><code>_</code></td>
|
|
262
311
|
<td>Current item during iteration</td>
|
|
263
|
-
<td><code
|
|
312
|
+
<td><code>=map(/items, _/name)</code></td>
|
|
264
313
|
</tr>
|
|
265
314
|
<tr>
|
|
266
315
|
<td><code>$event</code></td>
|
|
267
316
|
<td>Event object in handlers</td>
|
|
268
|
-
<td><code
|
|
317
|
+
<td><code>=set(=/selected, $event/target/value)</code></td>
|
|
269
318
|
</tr>
|
|
270
319
|
</tbody>
|
|
271
320
|
</table>
|
|
@@ -298,12 +347,12 @@
|
|
|
298
347
|
<tr>
|
|
299
348
|
<td><strong>Formulas</strong></td>
|
|
300
349
|
<td><code>=SUM(A1:A10)</code></td>
|
|
301
|
-
<td><code
|
|
350
|
+
<td><code>=sum(/items...price)</code></td>
|
|
302
351
|
</tr>
|
|
303
352
|
<tr>
|
|
304
353
|
<td><strong>Path Resolution</strong></td>
|
|
305
354
|
<td>Cell References (A1, $B$2)</td>
|
|
306
|
-
<td>JSON Pointer paths (./name,
|
|
355
|
+
<td>JSON Pointer paths (./name, =/global)</td>
|
|
307
356
|
</tr>
|
|
308
357
|
<tr>
|
|
309
358
|
<td><strong>Recalculation</strong></td>
|
|
@@ -317,79 +366,204 @@
|
|
|
317
366
|
<h2 id="integration">Lightview Integration</h2>
|
|
318
367
|
<p>
|
|
319
368
|
cDOM integrates seamlessly with Lightview's existing DOM formats. You can use JPRX expressions
|
|
320
|
-
in any of Lightview's hypermedia formats
|
|
369
|
+
in any of Lightview's hypermedia formats just by loading <code>lightview-cdom.js</code>:
|
|
321
370
|
</p>
|
|
322
|
-
|
|
323
371
|
<h3>vDOM (Virtual DOM)</h3>
|
|
324
|
-
<p>JPRX expressions work directly in vDOM arrays:</p>
|
|
372
|
+
<p>JPRX expressions work directly in vDOM arrays or objects:</p>
|
|
325
373
|
<div class="code-block">
|
|
326
|
-
<pre><code
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
374
|
+
<pre><code>// vDOM formal object
|
|
375
|
+
{
|
|
376
|
+
tag: "div",
|
|
377
|
+
attributes: { class: "counter" },
|
|
378
|
+
children: [
|
|
379
|
+
{ tag: "p", children: ["Count: ", "=/local/count"] },
|
|
380
|
+
{ tag: "button", attributes: { onclick: "=++/local/count }, children: ["+"] }
|
|
381
|
+
]
|
|
382
|
+
}</code></pre>
|
|
330
383
|
</div>
|
|
331
384
|
|
|
332
385
|
<h3>oDOM (Object DOM)</h3>
|
|
333
386
|
<p>JPRX expressions work in oDOM objects:</p>
|
|
334
387
|
<div class="code-block">
|
|
335
388
|
<pre><code>{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
389
|
+
div: {
|
|
390
|
+
class: "counter",
|
|
391
|
+
children: [
|
|
392
|
+
{ p: { children: ["Count: ", "=/local/count] } },
|
|
393
|
+
{ button: { onclick: =++/local/count, children: ["+"] } }
|
|
394
|
+
]
|
|
395
|
+
}
|
|
342
396
|
}</code></pre>
|
|
343
397
|
</div>
|
|
344
398
|
|
|
345
|
-
<h3>cDOM Shorthand</h3>
|
|
346
|
-
<p>cDOM's concise shorthand syntax
|
|
399
|
+
<h3>cDOM Shorthand with JPRX</h3>
|
|
400
|
+
<p>cDOM's concise shorthand syntax. Note that cDOM does not require quoting attribute names or JPRX
|
|
401
|
+
expressions like vDOM and oDOM do and you can include comments:</p>
|
|
347
402
|
<div class="code-block">
|
|
348
403
|
<pre><code>{
|
|
349
404
|
div: {
|
|
350
405
|
class: "counter",
|
|
406
|
+
// This is a JPRX comment
|
|
351
407
|
children: [
|
|
352
|
-
{ p: ["Count: ",
|
|
353
|
-
{ button: { onclick:
|
|
408
|
+
{ p: ["Count: ", =/local/count] },
|
|
409
|
+
{ button: { onclick: =++/local/count, children: ["+"] } }
|
|
354
410
|
]
|
|
355
411
|
}
|
|
356
412
|
}</code></pre>
|
|
357
413
|
</div>
|
|
358
414
|
|
|
359
|
-
<!-- =====
|
|
360
|
-
<h2 id="
|
|
415
|
+
<!-- ===== DOM PATCHES ===== -->
|
|
416
|
+
<h2 id="dom-patches">DOM Patches & Decentralized Layouts</h2>
|
|
361
417
|
<p>
|
|
362
|
-
cDOM
|
|
418
|
+
cDOM supports <strong>Decentralized Layouts</strong>, allowing components to "move themselves" to their
|
|
419
|
+
rightful
|
|
420
|
+
home in the DOM upon being created. This is especially powerful for LLM-driven streaming UIs.
|
|
363
421
|
</p>
|
|
364
422
|
|
|
365
|
-
<h3 id="
|
|
423
|
+
<h3 id="helpers-move">=move(target, location?)</h3>
|
|
366
424
|
<p>
|
|
367
|
-
|
|
425
|
+
The <code>=move</code> helper (typically used in <code>onmount</code>) teleports the host element to a
|
|
426
|
+
different part of the document.
|
|
368
427
|
</p>
|
|
369
428
|
<div class="code-block">
|
|
429
|
+
<pre><code>{
|
|
430
|
+
div: {
|
|
431
|
+
id: "weather-widget",
|
|
432
|
+
onmount: =move('#sidebar', 'afterbegin'),
|
|
433
|
+
content: "Sunny, 75°F"
|
|
434
|
+
}
|
|
435
|
+
}</code></pre>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<h4>Identity & Patching</h4>
|
|
439
|
+
<p>
|
|
440
|
+
If the moving element has a unique <code>id</code> and an element with that same ID already exists at the
|
|
441
|
+
destination,
|
|
442
|
+
the existing element is <strong>replaced</strong>. This turns <code>=move</code> into an idempotent
|
|
443
|
+
<strong>patch</strong>
|
|
444
|
+
command — simply stream the new version of the component with the same ID, and it will update the UI
|
|
445
|
+
automatically.
|
|
446
|
+
</p>
|
|
447
|
+
|
|
448
|
+
<h4>Placement Locations</h4>
|
|
449
|
+
<table class="api-table">
|
|
450
|
+
<thead>
|
|
451
|
+
<tr>
|
|
452
|
+
<th>Location</th>
|
|
453
|
+
<th>Result</th>
|
|
454
|
+
</tr>
|
|
455
|
+
</thead>
|
|
456
|
+
<tbody>
|
|
457
|
+
<tr>
|
|
458
|
+
<td><code>inner</code> / <code>shadow</code></td>
|
|
459
|
+
<td>Replaces all children of the target.</td>
|
|
460
|
+
</tr>
|
|
461
|
+
<tr>
|
|
462
|
+
<td><code>afterbegin</code> / <code>prepend</code></td>
|
|
463
|
+
<td>Inserts at the start of the target.</td>
|
|
464
|
+
</tr>
|
|
465
|
+
<tr>
|
|
466
|
+
<td><code>beforeend</code> / <code>append</code></td>
|
|
467
|
+
<td>Inserts at the end of the target (default).</td>
|
|
468
|
+
</tr>
|
|
469
|
+
<tr>
|
|
470
|
+
<td><code>outer</code> / <code>replace</code></td>
|
|
471
|
+
<td>Replaces the target node itself.</td>
|
|
472
|
+
</tr>
|
|
473
|
+
<tr>
|
|
474
|
+
<td><code>beforebegin</code> / <code>afterend</code></td>
|
|
475
|
+
<td>Inserts before or after the target node.</td>
|
|
476
|
+
</tr>
|
|
477
|
+
</tbody>
|
|
478
|
+
</table>
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
<h2 id="state-binding">State & Binding</h2>
|
|
482
|
+
<p>
|
|
483
|
+
cDOM does not use attribute directives for state it uses lifecycle events and helpers instead.
|
|
484
|
+
</p>
|
|
485
|
+
|
|
486
|
+
<h3 id="lifecycle-state">Lifecycle State</h3>
|
|
487
|
+
<p>
|
|
488
|
+
In Lightview, you initialize state within the <code>onmount</code> hook. The <code>=state</code> and
|
|
489
|
+
<code>=signal</code> helpers accept an options object where you can specify a <code>scope</code>,
|
|
490
|
+
as well as <code>schema</code> requirements.
|
|
491
|
+
</p>
|
|
492
|
+
|
|
493
|
+
<h4>Using a Registered Schema</h4>
|
|
494
|
+
<div class="code-block" style="margin-bottom: 1rem;">
|
|
495
|
+
<pre><code>// 1. Register centrally in JS
|
|
496
|
+
Lightview.registerSchema('User', { name: 'string', age: 'number' });
|
|
497
|
+
|
|
498
|
+
// 2. Use in cDOM
|
|
499
|
+
{ "onmount": "=state({}, { name: 'profile', schema: 'User', scope: $this })" }</code></pre>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<h4>Standard Schema Behaviors</h4>
|
|
503
|
+
<p>
|
|
504
|
+
When using the <code>schema</code> option, you can use these standard behaviors:
|
|
505
|
+
</p>
|
|
506
|
+
<ul style="margin-bottom: 2rem;">
|
|
507
|
+
<li><strong>"auto"</strong>: Infers a fixed schema from the initial value. Strict type checking (throws on
|
|
508
|
+
mismatch).</li>
|
|
509
|
+
<li><strong>"dynamic"</strong>: Like auto, but allows adding new properties to the state object.</li>
|
|
510
|
+
<li><strong>"polymorphic"</strong>: The most powerful setting. It includes <strong>"dynamic"</strong>
|
|
511
|
+
behavior and automatically <strong>coerces</strong> values to match the inferred type (e.g., "50" ->
|
|
512
|
+
50).
|
|
513
|
+
</li>
|
|
514
|
+
</ul>
|
|
515
|
+
|
|
516
|
+
<h4>Example: Polymorphic Coercion</h4>
|
|
517
|
+
<div class="code-block" style="margin-bottom: 2rem;">
|
|
370
518
|
<pre><code>{
|
|
371
519
|
div: {
|
|
372
|
-
"
|
|
520
|
+
"onmount": "=state({ count: 0 }, {
|
|
521
|
+
name: 'local',
|
|
522
|
+
schema: 'polymorphic',
|
|
523
|
+
scope: $this
|
|
524
|
+
})",
|
|
373
525
|
children: [
|
|
374
|
-
{ p: ["
|
|
375
|
-
{ p: ["Count: ", $/count] }
|
|
526
|
+
{ p: ["Typing '10' into a bind will save it as the number 10."] }
|
|
376
527
|
]
|
|
377
528
|
}
|
|
378
529
|
}</code></pre>
|
|
379
530
|
</div>
|
|
531
|
+
<p>
|
|
532
|
+
By scoping the state to the element, you can create multiple independent instances of components.
|
|
533
|
+
Lightview uses a high-performance <strong>up-tree search</strong> to resolve these names.
|
|
534
|
+
</p>
|
|
380
535
|
|
|
381
|
-
<h3 id="
|
|
536
|
+
<h3 id="bind-helper">Two-Way Binding ($bind)</h3>
|
|
382
537
|
<p>
|
|
383
|
-
|
|
538
|
+
Two-way data binding is achieved via the <code>=bind(path)</code> helper.
|
|
384
539
|
</p>
|
|
385
540
|
<div class="code-block">
|
|
386
|
-
<pre><code>{ input: { type: "text",
|
|
387
|
-
{ input: { type: "checkbox",
|
|
541
|
+
<pre><code>{ input: { type: "text", value: "=bind(/profile/name)", placeholder: "Enter name" } }
|
|
542
|
+
{ input: { type: "checkbox", checked: "=bind(/settings/enabled)" } }</code></pre>
|
|
388
543
|
</div>
|
|
544
|
+
|
|
545
|
+
<h4 id="binding-transformations">Handling Transformations</h4>
|
|
389
546
|
<p>
|
|
390
|
-
<
|
|
391
|
-
<code
|
|
547
|
+
Because <code>=bind</code> is strict (it only accepts direct paths), you cannot pass a computation
|
|
548
|
+
like <code>=bind(upper(/name))</code>. To transform data during binding, you have two choices:
|
|
392
549
|
</p>
|
|
550
|
+
<ul>
|
|
551
|
+
<li>
|
|
552
|
+
<strong>Manual Transformation:</strong> Use an <code>oninput</code> handler:
|
|
553
|
+
<code>{ oninput: "=set(/name, upper($event/target/value))" }</code>
|
|
554
|
+
</li>
|
|
555
|
+
<li>
|
|
556
|
+
<strong>Schema Transformation:</strong> Define a <code>transform</code> in your schema:
|
|
557
|
+
<div class="code-block" style="margin-top: 0.5rem;">
|
|
558
|
+
<pre><code>Lightview.registerSchema('Profile', {
|
|
559
|
+
name: { type: 'string', transform: 'upper' }
|
|
560
|
+
});</code></pre>
|
|
561
|
+
</div>
|
|
562
|
+
The state manager will automatically apply the <a
|
|
563
|
+
href="/docs/api/state.html#transformations">transformation</a>
|
|
564
|
+
during the write-back phase of <code>=bind</code>.
|
|
565
|
+
</li>
|
|
566
|
+
</ul>
|
|
393
567
|
|
|
394
568
|
<!-- ===== SHOPPING CART EXAMPLE ===== -->
|
|
395
569
|
<h2 id="shopping-cart">Shopping Cart Example</h2>
|
|
@@ -401,7 +575,7 @@
|
|
|
401
575
|
at: document.currentScript.parentElement,
|
|
402
576
|
scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
|
|
403
577
|
type: 'module',
|
|
404
|
-
height: '
|
|
578
|
+
height: '250px',
|
|
405
579
|
autoRun: true
|
|
406
580
|
});
|
|
407
581
|
</script><code contenteditable="true">// Shopping Cart: Demonstrating $map and $currency helpers
|
|
@@ -411,22 +585,22 @@ const { $ } = Lightview;
|
|
|
411
585
|
|
|
412
586
|
const cdomString = `{
|
|
413
587
|
div: {
|
|
414
|
-
"
|
|
588
|
+
"onmount": "=state({
|
|
415
589
|
cart: {
|
|
416
590
|
items: [
|
|
417
|
-
{ name:
|
|
418
|
-
{ name:
|
|
591
|
+
{ name: 'Apple', price: 1.00 },
|
|
592
|
+
{ name: 'Orange', price: 2.00 }
|
|
419
593
|
]
|
|
420
594
|
}
|
|
421
|
-
},
|
|
595
|
+
}, 'store')",
|
|
422
596
|
children: [
|
|
423
597
|
{ h3: "Shopping Cart" },
|
|
424
598
|
{ ul: {
|
|
425
|
-
children:
|
|
599
|
+
children: =map(/store/cart/items, { li: { children: [_/name, " - ", =currency(_/price)] } })
|
|
426
600
|
}},
|
|
427
601
|
{ p: {
|
|
428
602
|
style: "font-weight: bold; margin-top: 1rem;",
|
|
429
|
-
children: ["Total: ",
|
|
603
|
+
children: ["Total: ", =currency(sum(/store/cart/items...price))]
|
|
430
604
|
}}
|
|
431
605
|
]
|
|
432
606
|
}
|
|
@@ -434,8 +608,7 @@ const cdomString = `{
|
|
|
434
608
|
|
|
435
609
|
const hydrated = hydrate(parseJPRX(cdomString));
|
|
436
610
|
$('#example').content(hydrated);
|
|
437
|
-
|
|
438
|
-
</code></pre>
|
|
611
|
+
</code></pre></code></pre>
|
|
439
612
|
</div>
|
|
440
613
|
|
|
441
614
|
<!-- ===== INTERACTIVE EXAMPLE ===== -->
|
|
@@ -450,6 +623,8 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
|
|
|
450
623
|
(Standard)</button>
|
|
451
624
|
<button id="tab-btn-jprxc" class="syntax-tab" onclick="switchCDOMTab('jprxc')">JPRXC (Concise)</button>
|
|
452
625
|
<button id="tab-btn-operators" class="syntax-tab" onclick="switchCDOMTab('operators')">Operators</button>
|
|
626
|
+
<button id="tab-btn-embedded" class="syntax-tab" onclick="switchCDOMTab('embedded')">Embedded
|
|
627
|
+
Signals</button>
|
|
453
628
|
</div>
|
|
454
629
|
|
|
455
630
|
<!-- JPRX Pane -->
|
|
@@ -473,10 +648,10 @@ const cdomString = `{
|
|
|
473
648
|
"div": {
|
|
474
649
|
"children": [
|
|
475
650
|
{ "h3": ["Standard JPRX Counter"] },
|
|
476
|
-
{ "p": { "children": ["Count: ", "
|
|
651
|
+
{ "p": { "children": ["Count: ", "=/count"] }},
|
|
477
652
|
{ "div": { "children": [
|
|
478
|
-
{ "button": { "onclick": "
|
|
479
|
-
{ "button": { "onclick": "
|
|
653
|
+
{ "button": { "onclick": "=decrement(/count)", "children": ["-"] } },
|
|
654
|
+
{ "button": { "onclick": "=increment(/count)", "children": ["+"] } }
|
|
480
655
|
]}}
|
|
481
656
|
]
|
|
482
657
|
}
|
|
@@ -508,10 +683,10 @@ const cdomString = `{
|
|
|
508
683
|
div: {
|
|
509
684
|
children: [
|
|
510
685
|
{ h3: ["Concise Counter"] },
|
|
511
|
-
{ p: { children: ["Count: ",
|
|
686
|
+
{ p: { children: ["Count: ", =/count] }},
|
|
512
687
|
{ div: { children: [
|
|
513
|
-
{ button: { onclick:
|
|
514
|
-
{ button: { onclick:
|
|
688
|
+
{ button: { onclick: =decrement(/count), children: ["-"] } },
|
|
689
|
+
{ button: { onclick: =increment(/count), children: ["+"] } }
|
|
515
690
|
]}}
|
|
516
691
|
]
|
|
517
692
|
}
|
|
@@ -532,7 +707,7 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
|
|
|
532
707
|
type: 'module',
|
|
533
708
|
height: '250px'
|
|
534
709
|
});
|
|
535
|
-
</script><code contenteditable="true">// Operators: Using
|
|
710
|
+
</script><code contenteditable="true">// Operators: Using =++/path and =--/path
|
|
536
711
|
await import('/lightview-cdom.js');
|
|
537
712
|
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
538
713
|
const { signal, $ } = Lightview;
|
|
@@ -543,11 +718,11 @@ const cdomString = `{
|
|
|
543
718
|
div: {
|
|
544
719
|
children: [
|
|
545
720
|
{ h3: ["Operator Counter"] },
|
|
546
|
-
{ p: { children: ["Count: ",
|
|
721
|
+
{ p: { children: ["Count: ", =/count] }},
|
|
547
722
|
{ div: { children: [
|
|
548
|
-
// Prefix operators:
|
|
549
|
-
{ button: { onclick:
|
|
550
|
-
{ button: { onclick:
|
|
723
|
+
// Prefix operators: =-- and =++
|
|
724
|
+
{ button: { onclick: =--/count, children: ["-"] } },
|
|
725
|
+
{ button: { onclick: =++/count, children: ["+"] } }
|
|
551
726
|
]}}
|
|
552
727
|
]
|
|
553
728
|
}
|
|
@@ -559,10 +734,44 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
|
|
|
559
734
|
</code></pre>
|
|
560
735
|
</div>
|
|
561
736
|
|
|
737
|
+
<!-- Embedded Pane -->
|
|
738
|
+
<div id="pane-embedded" style="display: none;">
|
|
739
|
+
<pre><script>
|
|
740
|
+
examplify(document.currentScript.nextElementSibling, {
|
|
741
|
+
at: document.currentScript.parentElement,
|
|
742
|
+
scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
|
|
743
|
+
type: 'module',
|
|
744
|
+
height: '250px'
|
|
745
|
+
});
|
|
746
|
+
</script><code contenteditable="true">// Embedded Signals: Using onmount to initialize local state
|
|
747
|
+
await import('/lightview-cdom.js');
|
|
748
|
+
const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
|
|
749
|
+
const { $ } = Lightview;
|
|
750
|
+
|
|
751
|
+
const cdomString = `{
|
|
752
|
+
div: {
|
|
753
|
+
// Initialize a named signal explicitly scoped to '$this' element.
|
|
754
|
+
onmount: =signal(0, { name: 'count', scope: $this }),
|
|
755
|
+
children: [
|
|
756
|
+
{ h3: ["Embedded Counter"] },
|
|
757
|
+
{ p: ["Count: ", =/count] },
|
|
758
|
+
{ div: { children: [
|
|
759
|
+
{ button: { onclick: =--/count, children: ["-"] } },
|
|
760
|
+
{ button: { onclick: =++/count, children: ["+"] } }
|
|
761
|
+
]}}
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
}`;
|
|
765
|
+
|
|
766
|
+
const hydrated = hydrate(parseJPRX(cdomString));
|
|
767
|
+
$('#example').content(hydrated);
|
|
768
|
+
</code></pre>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
562
771
|
<!-- ===== OPERATOR SYNTAX EXAMPLE ===== -->
|
|
563
772
|
<h2 id="operator-syntax">Operator Syntax</h2>
|
|
564
773
|
<p>
|
|
565
|
-
JPRX
|
|
774
|
+
JPRX supports <strong>prefix</strong> and <strong>postfix</strong> operator syntax as alternatives to
|
|
566
775
|
function calls.
|
|
567
776
|
This makes expressions more concise and familiar to JavaScript developers.
|
|
568
777
|
</p>
|
|
@@ -578,18 +787,18 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
|
|
|
578
787
|
</thead>
|
|
579
788
|
<tbody>
|
|
580
789
|
<tr>
|
|
581
|
-
<td><code
|
|
582
|
-
<td><code
|
|
790
|
+
<td><code>=increment(/count)</code></td>
|
|
791
|
+
<td><code>=++/count</code> or <code>=/count++</code></td>
|
|
583
792
|
<td>Increment by 1</td>
|
|
584
793
|
</tr>
|
|
585
794
|
<tr>
|
|
586
|
-
<td><code
|
|
587
|
-
<td><code
|
|
795
|
+
<td><code>=decrement(/count)</code></td>
|
|
796
|
+
<td><code>=--/count</code> or <code>=/count--</code></td>
|
|
588
797
|
<td>Decrement by 1</td>
|
|
589
798
|
</tr>
|
|
590
799
|
<tr>
|
|
591
|
-
<td><code
|
|
592
|
-
<td><code
|
|
800
|
+
<td><code>=toggle(/enabled)</code></td>
|
|
801
|
+
<td><code>=!!/enabled</code></td>
|
|
593
802
|
<td>Toggle boolean (prefix only)</td>
|
|
594
803
|
</tr>
|
|
595
804
|
</tbody>
|
|
@@ -608,24 +817,31 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
|
|
|
608
817
|
Use standard event attributes with JPRX expressions:
|
|
609
818
|
</p>
|
|
610
819
|
<div class="code-block">
|
|
611
|
-
<pre><code>{ button: { onclick:
|
|
612
|
-
{ input: { oninput:
|
|
820
|
+
<pre><code>{ button: { onclick: =++/count, children: ["Click Me"] } }
|
|
821
|
+
{ input: { oninput: =set(/name, $event/target/value) } }</code></pre>
|
|
613
822
|
</div>
|
|
614
823
|
|
|
615
824
|
<h3 id="events-llm">LLM-Generated Interaction</h3>
|
|
616
825
|
<p>
|
|
617
|
-
LLMs can generate event handlers to register interest in user actions:
|
|
826
|
+
LLMs can generate event handlers to register interest in user actions or trigger UI updates:
|
|
618
827
|
</p>
|
|
619
828
|
<div class="code-block">
|
|
620
|
-
<pre><code
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}</code></pre>
|
|
829
|
+
<pre><code>// 1. Notify LLM of an event
|
|
830
|
+
{ button: { onclick: =fetch('/api/notify', { method: 'POST', body: $event }), children: ["Notify"] } }
|
|
831
|
+
|
|
832
|
+
// 2. Request a UI Patch from LLM
|
|
833
|
+
{ button: { onclick: =mount('/api/update', { method: 'POST', body: $event }), children: ["Update UI"] } }</code></pre>
|
|
626
834
|
</div>
|
|
627
835
|
<p>
|
|
628
|
-
The <code>$event</code> placeholder gives the LLM full context of the interaction.
|
|
836
|
+
The <code>$event</code> placeholder gives the LLM full context of the interaction. When using
|
|
837
|
+
<code>=mount</code>, the server should respond with cDOM, vDOM, or oDOM content (see the
|
|
838
|
+
<a href="#helpers-network">Network section</a> for more details).
|
|
839
|
+
</p>
|
|
840
|
+
<p>
|
|
841
|
+
By default, <code>=mount</code> will <strong>append</strong> the response to the <code>body</code> and
|
|
842
|
+
then let it "rip itself out" and <strong>teleport</strong> to its final destination if the response
|
|
843
|
+
contains a <code>=move</code> helper. This "Safe Landing" strategy ensures decentralized layouts
|
|
844
|
+
work seamlessly without the patcher needing to know the exact destination.
|
|
629
845
|
</p>
|
|
630
846
|
|
|
631
847
|
<h3 id="events-lifecycle">The Interaction Lifecycle</h3>
|
|
@@ -675,7 +891,7 @@ LightviewCDOM.activate(document.getElementById('myApp'));</code></pre>
|
|
|
675
891
|
Converts <code>$</code>-prefixed strings into reactive computed signals.
|
|
676
892
|
</p>
|
|
677
893
|
<div class="code-block">
|
|
678
|
-
<pre><code>const config = { title: "Dashboard", total: "
|
|
894
|
+
<pre><code>const config = { title: "Dashboard", total: "=/cart/items...price" };
|
|
679
895
|
const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
680
896
|
</div>
|
|
681
897
|
|
|
@@ -684,7 +900,18 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
684
900
|
Parses cDOM's concise syntax (unquoted keys, JPRX expressions) into a JavaScript object.
|
|
685
901
|
</p>
|
|
686
902
|
<div class="code-block">
|
|
687
|
-
<pre><code>const obj = LightviewCDOM.parseJPRX(`{ div: { children: [
|
|
903
|
+
<pre><code>const obj = LightviewCDOM.parseJPRX(`{ div: { children: [=/name] } }`);</code></pre>
|
|
904
|
+
</div>
|
|
905
|
+
|
|
906
|
+
<h3 id="api-registerSchema">registerSchema(name, definition)</h3>
|
|
907
|
+
<p>
|
|
908
|
+
Registers a reusable schema for state validation and initialization. Supported behaviors include
|
|
909
|
+
<code>"auto"</code> (strict/fixed), <code>"dynamic"</code> (strict/expandable), and
|
|
910
|
+
<code>"polymorphic"</code> (coerce/expandable).
|
|
911
|
+
</p>
|
|
912
|
+
<div class="code-block">
|
|
913
|
+
<pre><code>Lightview.registerSchema('User', { name: 'string', age: 'number' });
|
|
914
|
+
// Usable in JPRX: =state({}, { name: 'profile', schema: 'User' })</code></pre>
|
|
688
915
|
</div>
|
|
689
916
|
|
|
690
917
|
<h3 id="api-registerHelper">registerHelper(name, fn, options?)</h3>
|
|
@@ -693,7 +920,7 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
693
920
|
</p>
|
|
694
921
|
<div class="code-block">
|
|
695
922
|
<pre><code>LightviewCDOM.registerHelper('double', (x) => x * 2);
|
|
696
|
-
// Now usable:
|
|
923
|
+
// Now usable: =double(/count)</code></pre>
|
|
697
924
|
</div>
|
|
698
925
|
|
|
699
926
|
<!-- ===== JPRX HELPERS ===== -->
|
|
@@ -741,7 +968,7 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
741
968
|
</div>
|
|
742
969
|
|
|
743
970
|
<p class="text-sm italic opacity-80" style="margin-top: 1rem;">
|
|
744
|
-
Example: <code
|
|
971
|
+
Example: <code>=if(gt(=/count, 10), 'Large', 'Small')</code>
|
|
745
972
|
</p>
|
|
746
973
|
|
|
747
974
|
<h3 id="helpers-conditional">Conditional Aggregates</h3>
|
|
@@ -757,7 +984,7 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
757
984
|
</div>
|
|
758
985
|
<p class="text-xs italic opacity-70" style="margin-top: 0.5rem;">
|
|
759
986
|
<strong>Explosion Operator:</strong> In JPRX, <code>...</code> is placed at the end of a path
|
|
760
|
-
(e.g., <code
|
|
987
|
+
(e.g., <code>=/items...price</code>) to expand arrays into arguments.
|
|
761
988
|
</p>
|
|
762
989
|
|
|
763
990
|
<h3 id="helpers-datetime">DateTime</h3>
|
|
@@ -772,21 +999,63 @@ const liveConfig = LightviewCDOM.hydrate(config);</code></pre>
|
|
|
772
999
|
<pre><code>lookup, vlookup, index, match</code></pre>
|
|
773
1000
|
</div>
|
|
774
1001
|
|
|
775
|
-
<h3 id="helpers-mutation">State
|
|
776
|
-
<p>
|
|
1002
|
+
<h3 id="helpers-mutation">State & Lifecycle</h3>
|
|
1003
|
+
<p>Initialize and modify reactive state.</p>
|
|
777
1004
|
<div class="code-block">
|
|
778
|
-
<pre><code>set, increment (++), decrement (--), toggle (!!), push, pop, assign, clear</code></pre>
|
|
1005
|
+
<pre><code>state, signal, bind, set, increment (++), decrement (--), toggle (!!), push, pop, assign, clear</code></pre>
|
|
779
1006
|
</div>
|
|
780
1007
|
|
|
781
1008
|
<h3 id="helpers-network">Network</h3>
|
|
782
1009
|
<p>HTTP requests.</p>
|
|
783
1010
|
<div class="code-block">
|
|
784
|
-
<pre><code>fetch(url, options?)
|
|
1011
|
+
<pre><code>fetch(url, options?)
|
|
1012
|
+
<span id="helpers-mount"></span>mount(url, options?)</span></code></pre>
|
|
785
1013
|
</div>
|
|
786
|
-
<
|
|
787
|
-
|
|
788
|
-
|
|
1014
|
+
<table class="api-table">
|
|
1015
|
+
<thead>
|
|
1016
|
+
<tr>
|
|
1017
|
+
<th>Option</th>
|
|
1018
|
+
<th>Default</th>
|
|
1019
|
+
<th>Description</th>
|
|
1020
|
+
</tr>
|
|
1021
|
+
</thead>
|
|
1022
|
+
<tbody>
|
|
1023
|
+
<tr>
|
|
1024
|
+
<td><code>method</code></td>
|
|
1025
|
+
<td><code>"GET"</code></td>
|
|
1026
|
+
<td>HTTP method (GET, POST, etc.)</td>
|
|
1027
|
+
</tr>
|
|
1028
|
+
<tr>
|
|
1029
|
+
<td><code>body</code></td>
|
|
1030
|
+
<td><code>undefined</code></td>
|
|
1031
|
+
<td>Request body. If an <strong>object</strong>, it is automatically stringified to JSON and the
|
|
1032
|
+
<code>Content-Type</code> is set to <code>application/json</code>.
|
|
1033
|
+
</td>
|
|
1034
|
+
</tr>
|
|
1035
|
+
<tr>
|
|
1036
|
+
<td><code>headers</code></td>
|
|
1037
|
+
<td><code>{}</code></td>
|
|
1038
|
+
<td>Additional HTTP headers.</td>
|
|
1039
|
+
</tr>
|
|
1040
|
+
<tr>
|
|
1041
|
+
<td><code>target</code></td>
|
|
1042
|
+
<td><code>"body"</code></td>
|
|
1043
|
+
<td>(<code>mount</code> only) CSS selector for destination.</td>
|
|
1044
|
+
</tr>
|
|
1045
|
+
<tr>
|
|
1046
|
+
<td><code>location</code></td>
|
|
1047
|
+
<td><code>"beforeend"</code></td>
|
|
1048
|
+
<td>(<code>mount</code> only) Placement relative to target.</td>
|
|
1049
|
+
</tr>
|
|
1050
|
+
</tbody>
|
|
1051
|
+
</table>
|
|
1052
|
+
<p style="margin-top: 1rem;">
|
|
1053
|
+
The <code>mount</code> helper is the primary mechanism for **Hypermedia updates**. It fetches content
|
|
1054
|
+
(cDOM, vDOM, or oDOM) and injects it into the DOM. If the content contains a <code>=move</code>
|
|
1055
|
+
helper, it will automatically relocate itself upon mounting.
|
|
789
1056
|
</p>
|
|
790
1057
|
|
|
1058
|
+
|
|
1059
|
+
|
|
791
1060
|
</main>
|
|
792
|
-
</div>
|
|
1061
|
+
</div>
|