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.
@@ -2,7 +2,7 @@
2
2
  <script src="/lightview-router.js?base=/index.html"></script>
3
3
 
4
4
  <div class="docs-layout">
5
- <aside class="docs-sidebar" src="./nav.html"></aside>
5
+ <aside class="docs-sidebar" src="./nav.html" data-preserve-scroll="docs-nav"></aside>
6
6
 
7
7
  <main class="docs-content">
8
8
  <h1>State (Store)</h1>
@@ -11,7 +11,7 @@
11
11
  state tracks nested property changes automatically.
12
12
  </p>
13
13
 
14
- <h2>The Problem with Signals + Objects</h2>
14
+ <h2 id="shortcomings">The Shortcomings of Signals</h2>
15
15
  <pre><code>// Signals only react to reassignment
16
16
  const user = signal({ name: 'Alice', age: 25 });
17
17
 
@@ -23,7 +23,7 @@ const items = signal([1, 2, 3]);
23
23
  items.value.push(4); // ❌ Won't trigger updates!
24
24
  items.value = [...items.value, 4]; // ✅ Works, but tedious</code></pre>
25
25
 
26
- <h2>State to the Rescue</h2>
26
+ <h2 id="state-to-the-rescue">State to the Rescue</h2>
27
27
  <pre><code>const { state } = LightviewX;
28
28
 
29
29
  // Deep reactivity - mutations work!
@@ -38,7 +38,38 @@ items.push(4); // ✅ Triggers updates!
38
38
  items[0] = 10; // ✅ Triggers updates!
39
39
  items.sort(); // ✅ Triggers updates!</code></pre>
40
40
 
41
- <h2>Nested Objects</h2>
41
+ <h2 id="state-function">State Function</h2>
42
+ <p>The <code>state</code> function is the primary initializer for reactive stores.</p>
43
+ <div class="code-block">
44
+ <pre><code>state(initialValue, nameOrOptions?)</code></pre>
45
+ </div>
46
+ <h3>Parameters</h3>
47
+ <ul>
48
+ <li><code>initialValue</code>: The object or array to be wrapped in a reactive proxy.</li>
49
+ <li><code>nameOrOptions</code> (Optional):
50
+ <ul>
51
+ <li>If a <strong>string</strong>: This becomes the <code>name</code> of the state for global
52
+ registration.</li>
53
+ <li>If an <strong>object</strong>: Supported keys include:
54
+ <ul>
55
+ <li><code>name</code>: A unique identifier for the state in the registry.</li>
56
+ <li><code>storage</code>: An object implementing the Storage interface (e.g.,
57
+ <code>localStorage</code>) to enable persistence.
58
+ </li>
59
+ <li><code>scope</code>: A DOM element or object to bind the state's visibility for up-tree
60
+ lookups.</li>
61
+ <li><code>schema</code>: A validation behavior (<code>"auto"</code>, <code>"dynamic"</code>,
62
+ <code>"polymorphic"</code>) or a formal registered JSON Schema name (see <a
63
+ href="#json-schema-lite">JSON Schema Lite</a> below).
64
+ </li>
65
+ </ul>
66
+ </li>
67
+ </ul>
68
+ </li>
69
+ </ul>
70
+ <p>See the examples in the sections below for detailed usage of these parameters.</p>
71
+
72
+ <h2 id="nested-objects">Nested Objects</h2>
42
73
  <p>State tracks changes at any depth:</p>
43
74
  <pre><code>const app = state({
44
75
  user: {
@@ -58,33 +89,52 @@ app.user.profile.name = 'Bob';
58
89
  app.user.profile.settings.theme = 'light';
59
90
  app.items.push({ id: 1, text: 'Hello' });</code></pre>
60
91
 
61
- <h2>In the UI</h2>
92
+ <h2 id="in-the-ui">In the UI</h2>
93
+
94
+ <div id="ui-example">
95
+ <pre><script>
96
+ examplify(document.currentScript.nextElementSibling, {
97
+ at: document.currentScript.parentElement,
98
+ scripts: ['/lightview.js', '/lightview-x.js'],
99
+ type: 'module',
100
+ height: '200px',
101
+ autoRun: true,
102
+ controls: false
103
+ });
104
+ </script><code>const { state } = LightviewX;
105
+ const { tags, $ } = Lightview;
106
+ const { div, ul, li, input, span, button } = tags;
62
107
 
63
- <div class="code-example">
64
- <div class="code-example-preview" id="state-demo"></div>
65
- <div class="code-example-code">
66
- <pre><code>const todos = state([
108
+ const todos = state([
67
109
  { text: 'Learn Lightview', done: true },
68
110
  { text: 'Build app', done: false }
69
111
  ]);
70
112
 
71
- div(
72
- ul(() => todos.map((todo, i) =>
73
- li(
74
- input({
75
- type: 'checkbox',
76
- checked: todo.done,
77
- onchange: () => todos[i].done = !todos[i].done
78
- }),
79
- span(todo.text)
113
+ const app = div({ style: 'padding: 1rem;' },
114
+ ul({ style: 'list-style: none; padding: 0; margin-bottom: 1rem;' },
115
+ () => todos.map((todo, i) =>
116
+ li({ style: 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;' },
117
+ input({
118
+ type: 'checkbox',
119
+ checked: todo.done,
120
+ onchange: () => todos[i].done = !todos[i].done,
121
+ style: 'cursor: pointer;'
122
+ }),
123
+ span({
124
+ style: () => todo.done ? 'text-decoration: line-through; opacity: 0.6;' : ''
125
+ }, todo.text)
126
+ )
80
127
  )
81
- )),
82
- button({ onclick: () => todos.push({ text: 'New', done: false }) }, 'Add')
83
- )</code></pre>
84
- </div>
128
+ ),
129
+ button({
130
+ onclick: () => todos.push({ text: `Task ${todos.length + 1}`, done: false }),
131
+ }, '+ Add Task')
132
+ );
133
+
134
+ $('#example').content(app);</code></pre>
85
135
  </div>
86
136
 
87
- <h2>Array Methods</h2>
137
+ <h2 id="array-methods">Array Methods</h2>
88
138
  <p>All mutating array methods are reactive:</p>
89
139
  <pre><code>const items = state([1, 2, 3]);
90
140
 
@@ -96,8 +146,17 @@ items.splice(1, 1); // Remove at index
96
146
  items.sort(); // Sort in place
97
147
  items.reverse(); // Reverse in place
98
148
  items.fill(0); // Fill with value</code></pre>
149
+ <h2 id="date-methods">Date Methods</h2>
150
+ <p>When a property is a <code>Date</code> object, all mutating methods are reactive. This includes all
151
+ <code>set*</code> methods (e.g. <code>setFullYear()</code>, <code>setDate()</code>, <code>setHours()</code>,
152
+ etc.):
153
+ </p>
154
+ <pre><code>const event = state({ date: new Date() });
155
+
156
+ // This will trigger UI updates
157
+ event.date.setFullYear(2025);</code></pre>
99
158
 
100
- <h2>Named State</h2>
159
+ <h2 id="named-state">Named State</h2>
101
160
  <p>
102
161
  Like signals, you can name state objects for global access. This is especially useful for
103
162
  shared application state:
@@ -114,7 +173,7 @@ const globalState = state.get('app');
114
173
  // Get or create
115
174
  const settings = state.get('settings', { notifications: true });</code></pre>
116
175
 
117
- <h2>Stored State</h2>
176
+ <h2 id="stored-state">Stored State</h2>
118
177
  <p>
119
178
  You can store named state objects in Storage objects (e.g. sessionStorage or localStorage) for persistence.
120
179
  It will be saved any time there is a change. Objects are automatically
@@ -130,83 +189,222 @@ const sameUser = state.get('user');
130
189
  const score = state.get('user', {storage:sessionStorage, defaultValue:{name:'Guest', theme:'dark'}});</code></pre>
131
190
  <p>Note: Manually updating the object in storage will not trigger updates.</p>
132
191
 
133
- <h2>Signal vs State</h2>
192
+ <h2 id="schema-validation">Schema Validation</h2>
193
+ <p>
194
+ State objects can be validated and transformed using the <code>schema</code> option. This ensures data
195
+ integrity and can automatically coerce values to the expected types.
196
+ </p>
197
+ <pre><code>const user = state({ name: 'Alice', age: 25 }, { schema: 'auto' });
198
+
199
+ user.age = 26; // ✅ Works (same type)
200
+ user.age = '30'; // ❌ Throws: Type mismatch
201
+ user.status = 'active'; // ❌ Throws: Cannot add new property</code></pre>
202
+
203
+ <h3>Built-in Schema Behaviors</h3>
134
204
  <table class="api-table">
135
205
  <thead>
136
206
  <tr>
137
- <th>Use Signal</th>
138
- <th>Use State</th>
207
+ <th>Behavior</th>
208
+ <th>Description</th>
139
209
  </tr>
140
210
  </thead>
141
211
  <tbody>
142
212
  <tr>
143
- <td>Primitives (numbers, strings, bools)</td>
144
- <td>Objects with nested properties</td>
213
+ <td><code>"auto"</code></td>
214
+ <td>Infers a fixed schema from the initial value. Prevents adding new properties and enforces strict
215
+ type checking.</td>
145
216
  </tr>
146
217
  <tr>
147
- <td>Simple objects (replace whole thing)</td>
148
- <td>Objects you'll mutate in place</td>
218
+ <td><code>"dynamic"</code></td>
219
+ <td>Like <code>auto</code>, but allows the state object to grow with new properties.</td>
149
220
  </tr>
150
221
  <tr>
151
- <td>Arrays you'll replace</td>
152
- <td>Arrays you'll push/pop/splice</td>
222
+ <td><code>"polymorphic"</code></td>
223
+ <td>Allows growth and automatically <strong>coerces</strong> values to match the initial type (e.g.,
224
+ setting "100" to a number property saves it as the number 100).</td>
153
225
  </tr>
226
+ </tbody>
227
+ </table>
228
+ <pre><code>// Polymorphic coerces types automatically
229
+ const settings = state({ volume: 50, muted: false }, { schema: 'polymorphic' });
230
+
231
+ settings.volume = '75'; // ✅ Coerced to number 75
232
+ settings.muted = 'true'; // ✅ Coerced to boolean true
233
+ settings.newProp = 'ok'; // ✅ Allowed (dynamic growth)</code></pre>
234
+
235
+ <h3 id="json-schema-lite">JSON Schema Lite</h3>
236
+ <p>
237
+ When you load <code>lightview-x.js</code>, the schema engine is upgraded to a "JSON Schema Lite" validator.
238
+ It supports standard Draft 7 keywords while remaining incredibly lightweight.
239
+ </p>
240
+
241
+ <h4>Supported Keywords</h4>
242
+ <table class="api-table">
243
+ <thead>
154
244
  <tr>
155
- <td>Slightly better performance</td>
156
- <td>More convenient API</td>
245
+ <th>Type</th>
246
+ <th>Keywords</th>
247
+ </tr>
248
+ </thead>
249
+ <tbody>
250
+ <tr>
251
+ <td><strong>String</strong></td>
252
+ <td><code>minLength</code>, <code>maxLength</code>, <code>pattern</code>,
253
+ <code>format: 'email'</code>
254
+ </td>
255
+ </tr>
256
+ <tr>
257
+ <td><strong>Number</strong></td>
258
+ <td><code>minimum</code>, <code>maximum</code>, <code>multipleOf</code></td>
259
+ </tr>
260
+ <tr>
261
+ <td><strong>Object</strong></td>
262
+ <td><code>required</code>, <code>properties</code>, <code>additionalProperties: false</code></td>
263
+ </tr>
264
+ <tr>
265
+ <td><strong>Array</strong></td>
266
+ <td><code>items</code>, <code>minItems</code>, <code>maxItems</code>, <code>uniqueItems</code></td>
267
+ </tr>
268
+ <tr>
269
+ <td><strong>General</strong></td>
270
+ <td><code>type</code>, <code>enum</code>, <code>const</code></td>
157
271
  </tr>
158
272
  </tbody>
159
273
  </table>
160
274
 
161
- <h2>Pro Tip</h2>
275
+ <h4>Named Schemas</h4>
162
276
  <p>
163
- You can mix both! Use signals for simple values and state for complex structures:
277
+ You can register schemas globally and reference them by name. This encourages reuse and keeps your
278
+ <code>state</code> initializations clean.
164
279
  </p>
165
- <pre><code>const isLoading = signal(false); // Simple boolean → signal
166
- const error = signal(null); // Simple value → signal
167
- const items = state([]); // Array to mutate → state (from LightviewX)
168
- const formData = state({ // Object to mutate → state (from LightviewX)
169
- name: '',
170
- email: '',
171
- message: ''
172
- });</code></pre>
280
+ <pre><code>// 1. Register a schema
281
+ Lightview.registerSchema('User', {
282
+ type: "object",
283
+ properties: {
284
+ username: { type: "string", minLength: 3 },
285
+ email: { type: "string", format: "email" }
286
+ },
287
+ required: ["username"]
288
+ });
289
+
290
+ // 2. Use it by name
291
+ const user = state({ username: "alice" }, { schema: "User" });</code></pre>
292
+
293
+ <h3 id="transformations">Transformation Helpers</h3>
294
+ <p>
295
+ Unlike industry-standard validators (which only allow/disallow data), Lightview's engine supports
296
+ <strong>declarative transformations</strong>. This allows you to specify how data should be "cleaned" before
297
+ it hits the state.
298
+ </p>
299
+ <pre><code>const UserSchema = {
300
+ type: "object",
301
+ properties: {
302
+ username: {
303
+ type: "string",
304
+ transform: "lower" // Using a registered helper name
305
+ },
306
+ salary: {
307
+ type: "number",
308
+ transform: (v) => Math.round(v) // Using an inline function
309
+ }
310
+ }
311
+ };
312
+
313
+ const user = state({ username: 'ALICE', salary: 50212.55 }, { schema: UserSchema });
314
+ // user.username is 'alice' (via "lower")
315
+ // user.salary is 50213 (via inline function)</code></pre>
316
+
317
+ <p>
318
+ When using a <strong>string</strong> for the transform (like <code>"lower"</code>), Lightview looks for a
319
+ matching function in the global registry. This requires that the helper has been registered via JPRX
320
+ (requires <code>lightview-cdom.js</code>) or manually in <code>Lightview.helpers</code>. For more details on
321
+ built-in
322
+ helpers, see the <a href="/docs/cdom.html">cDOM documentation</a>.
323
+ </p>
324
+
325
+ <div class="code-block info"
326
+ style="border-left: 4px solid #3b82f6; padding: 1rem; background: #eff6ff; margin-bottom: 2rem;">
327
+ <strong>🔍 Naming Tip:</strong> When using string names for transforms, use the <strong>bare helper
328
+ name</strong> (e.g., <code>"lower"</code>, <code>"round"</code>). You do <strong>not</strong> need a
329
+ leading <code>$</code>.
330
+ </div>
331
+
332
+ <h4>Loading Helpers</h4>
333
+ <p>
334
+ When using the <code>transform</code> keyword, Lightview searches for the named function in the following
335
+ order:
336
+ </p>
337
+ <ol style="margin-bottom: 2rem;">
338
+ <li><strong>JPRX Helpers</strong>: Any helper registered via <code>registerHelper</code> (requires
339
+ <code>lightview-cdom.js</code>).
340
+ </li>
341
+ <li><strong>Public API</strong>: Functions attached to <code>Lightview.helpers</code>.</li>
342
+ <li><strong>Inline</strong>: You can also pass a function directly to the <code>transform</code> property.
343
+ </li>
344
+ </ol>
345
+
346
+ <div class="code-block info"
347
+ style="border-left: 4px solid #3b82f6; padding: 1rem; background: #eff6ff; margin-bottom: 2rem;">
348
+ <strong>💡 Note:</strong> To use JPRX helpers (like <code>math</code>, <code>string</code>, or
349
+ <code>array</code> helpers) for state transformations, you must ensure <code>lightview-cdom.js</code> is
350
+ loaded before your state is initialized.
351
+ </div>
352
+
353
+ <h3 id="lite-vs-full">Lite vs. Full Validators</h3>
354
+ <p>
355
+ Why use our "Lite" engine instead of a full validator like <strong>Ajv</strong>?
356
+ </p>
357
+ <table class="api-table">
358
+ <thead>
359
+ <tr>
360
+ <th>Feature</th>
361
+ <th>Lightview Lite</th>
362
+ <th>Full (Ajv, etc.)</th>
363
+ </tr>
364
+ </thead>
365
+ <tbody>
366
+ <tr>
367
+ <td><strong>Size</strong></td>
368
+ <td>~2KB (built-in)</td>
369
+ <td>~40KB+ (external)</td>
370
+ </tr>
371
+ <tr>
372
+ <td><strong>Transformation</strong></td>
373
+ <td>✅ First-class support</td>
374
+ <td>❌ Not supported (Validators only)</td>
375
+ </tr>
376
+ <tr>
377
+ <td><strong>Performance</strong></td>
378
+ <td>🚀 Optimized for UI cycles</td>
379
+ <td>Heavy overhead for simple checks</td>
380
+ </tr>
381
+ <tr>
382
+ <td><strong>Spec Compliance</strong></td>
383
+ <td>Basic Draft 7 (No <code>$ref</code>)</td>
384
+ <td>100% Full Specification</td>
385
+ </tr>
386
+ </tbody>
387
+ </table>
388
+
389
+ <h4 id="full-validator">Using a Full Validator</h4>
390
+ <p>
391
+ If you have complex enterprise schemas that require full specification compliance, you can swap out the
392
+ Lightview engine for any industry-standard validator:
393
+ </p>
394
+ <div class="code-block warning"
395
+ style="border-left: 4px solid #f59e0b; padding: 1rem; background: #fffbeb; margin-bottom: 2rem;">
396
+ <strong>⚠️ Warning:</strong> If you swap the validator, you will lose support for the declarative
397
+ <code>transform</code> keyword in your schemas unless your external validator also supports it.
398
+ </div>
399
+ <pre><code>import Ajv from "ajv";
400
+ const ajv = new Ajv();
401
+
402
+ // Swap the validation hook
403
+ Lightview.internals.hooks.validate = (value, schema) => {
404
+ const valid = ajv.validate(schema, value);
405
+ if (!valid) throw new Error(ajv.errorsText());
406
+ return true;
407
+ };</code></pre>
408
+
173
409
  </main>
174
- </div>
175
-
176
- <script>
177
- (function () {
178
- const { tags } = Lightview;
179
- const { state } = LightviewX;
180
- const { div, ul, li, input, span, button } = tags;
181
-
182
- const todos = state([
183
- { text: 'Learn Lightview', done: true },
184
- { text: 'Build something', done: false }
185
- ]);
186
-
187
- const demo = div({ style: 'padding: 0.5rem;' },
188
- ul({ style: 'list-style: none; padding: 0; margin: 0 0 1rem;' },
189
- () => todos.map((todo, i) =>
190
- li({ style: 'display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0;' },
191
- input({
192
- type: 'checkbox',
193
- checked: todo.done,
194
- onchange: () => todos[i].done = !todos[i].done,
195
- style: 'cursor: pointer;'
196
- }),
197
- span({
198
- style: () => `${todo.done ? 'text-decoration: line-through; opacity: 0.6;' : ''}`
199
- }, todo.text)
200
- )
201
- )
202
- ),
203
- button({
204
- onclick: () => todos.push({ text: `Task ${todos.length + 1}`, done: false }),
205
- style: 'padding: 0.5rem 1rem; cursor: pointer; background: var(--site-primary); color: white; border: none; border-radius: 6px;'
206
- }, '+ Add Task')
207
- );
208
-
209
- const container = document.getElementById('state-demo');
210
- if (container) container.appendChild(demo.domEl);
211
- })();
212
- </script>
410
+ </div>
@@ -3,7 +3,7 @@ var examplifyIdCounter = globalThis.examplifyIdCounter || 0;
3
3
  globalThis.examplifyIdCounter = examplifyIdCounter;
4
4
 
5
5
  globalThis.examplify = function examplify(target, options = {}) {
6
- const { scripts, styles, modules, html, at, location = 'beforeBegin', type, height, minHeight = 100, maxHeight = Infinity, allowSameOrigin = false, useOrigin = null, language = 'js', autoRun = false } = options;
6
+ const { scripts, styles, modules, html, at, location = 'beforeBegin', type, height, minHeight = 100, maxHeight = Infinity, allowSameOrigin = false, useOrigin = null, language = 'js', autoRun = false, controls: showControls = true } = options;
7
7
  const originalContent = target.textContent;
8
8
  const autoResize = !height; // Auto-resize if no explicit height is provided
9
9
  const iframeId = `examplify-${++examplifyIdCounter}`;
@@ -17,6 +17,7 @@ globalThis.examplify = function examplify(target, options = {}) {
17
17
  const controls = document.createElement('div');
18
18
  const editable = target.getAttribute('contenteditable') == 'true';
19
19
  controls.className = 'examplify-controls';
20
+ if (!showControls) controls.style.display = 'none';
20
21
 
21
22
  // Controls HTML
22
23
  controls.innerHTML = `
@@ -9,6 +9,7 @@
9
9
  <div class="docs-nav-title">JPRX</div>
10
10
  <a href="#JPRX" class="docs-nav-link">Introduction</a>
11
11
  <a href="#JPRX-delimiters" class="docs-nav-link">Delimiters</a>
12
+ <a href="#JPRX-escaping" class="docs-nav-link">Escaping</a>
12
13
  <a href="#JPRX-anatomy" class="docs-nav-link">Anatomy of a Path</a>
13
14
  <a href="#JPRX-placeholders" class="docs-nav-link">Placeholders</a>
14
15
  </div>
@@ -16,7 +17,7 @@
16
17
  <div class="docs-nav-title">Concepts</div>
17
18
  <a href="#comparison" class="docs-nav-link">Comparison to Excel</a>
18
19
  <a href="#integration" class="docs-nav-link">Lightview Integration</a>
19
- <a href="#directives" class="docs-nav-link">Directives</a>
20
+ <a href="#dom-patches" class="docs-nav-link">Decentralized Layouts (=move)</a>
20
21
  </div>
21
22
  <div class="docs-nav-section">
22
23
  <div class="docs-nav-title">Examples</div>
@@ -48,7 +49,7 @@
48
49
  <a href="#helpers-datetime" class="docs-nav-link" style="font-size: 0.85em;">DateTime</a>
49
50
  <a href="#helpers-lookup" class="docs-nav-link" style="font-size: 0.85em;">Lookup</a>
50
51
  <a href="#helpers-mutation" class="docs-nav-link" style="font-size: 0.85em;">State Mutation</a>
51
- <a href="#helpers-network" class="docs-nav-link" style="font-size: 0.85em;">Network</a>
52
+ <a href="#helpers-network" class="docs-nav-link" style="font-size: 0.85em;">Network (=mount)</a>
52
53
  </div>
53
54
  </div>
54
55
  </div>