@lmjs/core 2.0.0 → 2.1.0
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/dist/lumenjs-core-with-plugins.js +6 -6
- package/dist/lumenjs-core.js +7 -7
- package/package.json +1 -1
- package/src/_re.js +608 -77
- package/src/dom-shim.js +167 -0
- package/src/index-bootstrap.js +7 -1
- package/src/lstnrs.js +20 -2
- package/src/walk.js +101 -13
- package/vendor/reconnecting-websocket.js +25 -0
package/src/_re.js
CHANGED
|
@@ -17,6 +17,7 @@ class _v {
|
|
|
17
17
|
static name;
|
|
18
18
|
static type;
|
|
19
19
|
static vars;
|
|
20
|
+
static fns;
|
|
20
21
|
static rvs;
|
|
21
22
|
static _pv;
|
|
22
23
|
static mx;
|
|
@@ -29,6 +30,16 @@ class _v {
|
|
|
29
30
|
this.hst = obj.hst ?? [];
|
|
30
31
|
this.views = obj.views ?? [];
|
|
31
32
|
this.vars = obj.vars ?? {};
|
|
33
|
+
// 2026-09-17: per-instance event-handler functions (see walk.js's
|
|
34
|
+
// matching addition, and lstnrs.js's evalEvAttr) — a subview's own
|
|
35
|
+
// `function bump(){}` still becomes a real `window.bump` too (plain
|
|
36
|
+
// <script>-tag semantics, unchanged), but multiple mounted
|
|
37
|
+
// instances of the same subview file all define a same-named
|
|
38
|
+
// function, so window.bump silently becomes whichever instance's
|
|
39
|
+
// script ran last — every instance's @click ended up calling THAT
|
|
40
|
+
// one instance's handler regardless of which was actually clicked.
|
|
41
|
+
// .fns is this instance's own, unambiguous copy.
|
|
42
|
+
this.fns = obj.fns ?? {};
|
|
32
43
|
this.rvs = obj.rvs ?? {};
|
|
33
44
|
this._pv = obj._pv ?? null;
|
|
34
45
|
this.mx = obj.mx ?? [];
|
|
@@ -110,6 +121,67 @@ function reportLumenError(info) {
|
|
|
110
121
|
|
|
111
122
|
function _x(_x) {
|
|
112
123
|
var currPath = [];
|
|
124
|
+
// 2026-09-17: generalized from a position-specific check
|
|
125
|
+
// (`currPath[1] == "vars"`, only ever matched a flat `_vt.<root>.vars`
|
|
126
|
+
// write) to a real owner-walk — needed for per-instance subview
|
|
127
|
+
// scoping (`_vt.View.views[i].vars`, and its nested form
|
|
128
|
+
// `_vt.View.views[0].views[2].vars`), where "vars" can sit at any
|
|
129
|
+
// depth. `path` is currPath *without* the trailing "vars" segment.
|
|
130
|
+
// `Global` is a real special case, not a bug in this generalization:
|
|
131
|
+
// it has no `._re` of its own (see the object literal below — just
|
|
132
|
+
// `{vars:{}}`) since Global-var writes need to re-render whichever
|
|
133
|
+
// view is *currently* looking at that value, exactly like the
|
|
134
|
+
// pre-existing behavior this preserves. Everything else (View itself,
|
|
135
|
+
// or any views[i]/views[i].views[j]/... chain) is walked against the
|
|
136
|
+
// raw (non-proxied) tree — using the outer `_x` param directly, not
|
|
137
|
+
// the `_vt` proxy — so this traversal itself can't re-enter these
|
|
138
|
+
// traps and corrupt the in-flight currPath.
|
|
139
|
+
function _dispatchVarsUpdate(key) {
|
|
140
|
+
// "vars" can be anywhere in currPath, not just at index 1 — find
|
|
141
|
+
// it. The var name to update() is whatever comes right after it
|
|
142
|
+
// in currPath (e.g. `_vt.View.vars["obj"].nested = x` — the write
|
|
143
|
+
// itself is on "obj"'s own proxy, so `key` here is "nested", but
|
|
144
|
+
// the reactive var is "obj", one segment before it in currPath);
|
|
145
|
+
// if "vars" is the *last* segment, there's no next segment yet —
|
|
146
|
+
// that means this write IS the var (`_vt.View.vars["x"] = ...`),
|
|
147
|
+
// so `key` itself (the set() trap's own key) is the var name.
|
|
148
|
+
let varsIdx = currPath.indexOf("vars");
|
|
149
|
+
if (varsIdx === -1) return;
|
|
150
|
+
let varName = varsIdx < currPath.length - 1 ? currPath[varsIdx + 1] : key;
|
|
151
|
+
let rootPath = currPath.slice(0, varsIdx);
|
|
152
|
+
// Global has no ._re of its own (see its plain {vars:{}} literal
|
|
153
|
+
// below) — a Global write always re-renders whichever view is
|
|
154
|
+
// currently looking at it, exactly like the pre-existing behavior
|
|
155
|
+
// this preserves, for both the flat and nested-property case.
|
|
156
|
+
if (rootPath[0] === "Global") {
|
|
157
|
+
// 2026-09-18, real gap found and fixed: this file's own design
|
|
158
|
+
// comment above _vt's declaration says a Global var should be
|
|
159
|
+
// usable by "ordinary view code" just by referencing the
|
|
160
|
+
// name — but nothing ever made that true for a VIEW's own
|
|
161
|
+
// <script> statements (as opposed to mustaches/:if conditions,
|
|
162
|
+
// which already fall back to _vt.Global.vars via getVal()/
|
|
163
|
+
// evalExp()'s own merge). A view's compiled script only
|
|
164
|
+
// rewrites references to names IT ITSELF declares — it has no
|
|
165
|
+
// way to know index.js declared some other name as Global, so
|
|
166
|
+
// a bare reference to it (e.g. `rx.post(...)` where `rx =
|
|
167
|
+
// new RX(...)` was set in index.js's init()) resolved via the
|
|
168
|
+
// real JS scope chain, which only ever finds a real `window.rx`
|
|
169
|
+
// if something put it there — nothing did. Mirroring every
|
|
170
|
+
// Global write onto `window[varName]` too makes plain bare-
|
|
171
|
+
// identifier references resolve correctly and naturally,
|
|
172
|
+
// with zero per-script rewriting needed.
|
|
173
|
+
try {
|
|
174
|
+
if (typeof window !== 'undefined') window[varName] = _x.Global.vars[varName];
|
|
175
|
+
} catch (e) { }
|
|
176
|
+
if (_vt.View._re) _vt.View._re.update(varName);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let owner = _x;
|
|
180
|
+
for (let i = 0; i < rootPath.length && owner; i++) {
|
|
181
|
+
owner = owner[rootPath[i]];
|
|
182
|
+
}
|
|
183
|
+
if (owner && owner._re) owner._re.update(varName);
|
|
184
|
+
}
|
|
113
185
|
const handler = {
|
|
114
186
|
get(target, key) {
|
|
115
187
|
if (key == "__isProxy") return true;
|
|
@@ -118,7 +190,17 @@ function _x(_x) {
|
|
|
118
190
|
// so a stale tail from a prior _vt.Global read (which never hit
|
|
119
191
|
// "View" to reset it) would otherwise get prepended to the next
|
|
120
192
|
// chain and corrupt which key set()'s update(key) fires for.
|
|
121
|
-
|
|
193
|
+
// 2026-09-18: generalized from a hardcoded `key == "View" ||
|
|
194
|
+
// key == "Global"` name list (which needed editing every time a
|
|
195
|
+
// new permanent root, e.g. "Widgets", got added) to the one
|
|
196
|
+
// thing that's actually invariant: `target === _x` is only ever
|
|
197
|
+
// true for THIS proxy's own outermost target (the raw root
|
|
198
|
+
// object this whole closure was built from) — any nested proxy
|
|
199
|
+
// in the tree wraps some OTHER object as its target, never `_x`
|
|
200
|
+
// itself. So this fires on any fresh top-level access
|
|
201
|
+
// (`_vt.View`, `_vt.Global`, `_vt.Widgets`, ...) regardless of
|
|
202
|
+
// the key name, with nothing new to maintain per root added.
|
|
203
|
+
if (target === _x) currPath = [];
|
|
122
204
|
currPath.push(key);
|
|
123
205
|
if (typeof target[key] === 'object' && target[key] !== null && key != "_re") {
|
|
124
206
|
// cl("salsaa", target, key)
|
|
@@ -136,31 +218,12 @@ function _x(_x) {
|
|
|
136
218
|
// var _View = currPath[1] == "views" ? currPath[2] : "";
|
|
137
219
|
|
|
138
220
|
try {
|
|
139
|
-
// cl(["hiiiiii", currPath, key, value]);
|
|
140
221
|
// `_vt.View._re` genuinely doesn't exist yet during
|
|
141
222
|
// index.js's own bootstrap (before any view has mounted —
|
|
142
223
|
// see index-bootstrap.js) — that's the normal case now,
|
|
143
|
-
// not an error, so it's guarded
|
|
144
|
-
// throw into the catch below
|
|
145
|
-
|
|
146
|
-
if (_vt.View._re) _vt.View._re.update(key);
|
|
147
|
-
} else if (currPath.length > 2 && currPath[1] == "vars") {
|
|
148
|
-
let ky = clone(currPath[2]);
|
|
149
|
-
if (_vt.View._re) _vt.View._re.update(ky);
|
|
150
|
-
}
|
|
151
|
-
// if (currPath.length) {
|
|
152
|
-
// _vt.View._re.set(key, value);
|
|
153
|
-
// }
|
|
154
|
-
// cl(currPath, _View);
|
|
155
|
-
// eval("var kira = " + "_vt." + currPath.join("."));
|
|
156
|
-
// if (kira.__isProxy) {
|
|
157
|
-
// currPath.push(key);
|
|
158
|
-
// eval("kira = " + "_vt." + currPath.join("."));
|
|
159
|
-
// }
|
|
160
|
-
|
|
161
|
-
// if (kira) {
|
|
162
|
-
// // if (_el.data('_re')) _el.data('_re').set(key, value);
|
|
163
|
-
// }
|
|
224
|
+
// not an error, so it's guarded (inside _dispatchVarsUpdate)
|
|
225
|
+
// rather than left to throw into the catch below.
|
|
226
|
+
_dispatchVarsUpdate(key);
|
|
164
227
|
} catch (e) {
|
|
165
228
|
cl(e);
|
|
166
229
|
}
|
|
@@ -175,31 +238,7 @@ function _x(_x) {
|
|
|
175
238
|
delete target[key];
|
|
176
239
|
|
|
177
240
|
try {
|
|
178
|
-
|
|
179
|
-
// `_vt.View._re` genuinely doesn't exist yet during
|
|
180
|
-
// index.js's own bootstrap (before any view has mounted —
|
|
181
|
-
// see index-bootstrap.js) — that's the normal case now,
|
|
182
|
-
// not an error, so it's guarded here rather than left to
|
|
183
|
-
// throw into the catch below on every such write.
|
|
184
|
-
if (currPath.length == 2 && currPath[1] == "vars") {
|
|
185
|
-
if (_vt.View._re) _vt.View._re.update(key);
|
|
186
|
-
} else if (currPath.length > 2 && currPath[1] == "vars") {
|
|
187
|
-
let ky = clone(currPath[2]);
|
|
188
|
-
if (_vt.View._re) _vt.View._re.update(ky);
|
|
189
|
-
}
|
|
190
|
-
// if (currPath.length) {
|
|
191
|
-
// _vt.View._re.set(key, value);
|
|
192
|
-
// }
|
|
193
|
-
// cl(currPath, _View);
|
|
194
|
-
// eval("var kira = " + "_vt." + currPath.join("."));
|
|
195
|
-
// if (kira.__isProxy) {
|
|
196
|
-
// currPath.push(key);
|
|
197
|
-
// eval("kira = " + "_vt." + currPath.join("."));
|
|
198
|
-
// }
|
|
199
|
-
|
|
200
|
-
// if (kira) {
|
|
201
|
-
// // if (_el.data('_re')) _el.data('_re').set(key, value);
|
|
202
|
-
// }
|
|
241
|
+
_dispatchVarsUpdate(key);
|
|
203
242
|
} catch (e) {
|
|
204
243
|
cl(e);
|
|
205
244
|
}
|
|
@@ -240,11 +279,73 @@ function _x(_x) {
|
|
|
240
279
|
// its vars (V1's documented convention for app-wide state like `isAdmin`)
|
|
241
280
|
// need to outlive any single view. concatVarsAtLevel() below merges it into
|
|
242
281
|
// every scope lookup so ordinary view code can just reference the name.
|
|
282
|
+
// 2026-09-17, real bug found and fixed: `.fns` was missing here (only
|
|
283
|
+
// `.vars` existed) — `walk.js`'s `_fnRegisterStatement()` (added for
|
|
284
|
+
// per-instance subview scoping, see the per-instance-scoping work above)
|
|
285
|
+
// unconditionally builds `<targetRoot>.fns["name"] = name;` for EVERY
|
|
286
|
+
// top-level function declaration, regardless of target — including
|
|
287
|
+
// `targetKey: "Global"`, used to compile index.js/project scripts. Any
|
|
288
|
+
// real project with so much as one function declaration in index.js (an
|
|
289
|
+
// extremely common pattern) crashed with "Cannot set properties of
|
|
290
|
+
// undefined (setting '<fnName>')" the moment that line ran. Nearly
|
|
291
|
+
// invisible in `lm serve` dev mode — index-bootstrap.js's synchronous XHR
|
|
292
|
+
// fetch-and-eval of the compiled index.js is wrapped in a try/catch that
|
|
293
|
+
// only `console.log`s the failure (never `console.error`/throws), so nothing
|
|
294
|
+
// looked broken as long as the crash landed after anything load-bearing
|
|
295
|
+
// (Reactor()) in that same script. Fatal in a real `lm build` production
|
|
296
|
+
// bundle, where the compiled index.js is concatenated inline, unguarded —
|
|
297
|
+
// an uncaught exception there aborts every remaining statement in the
|
|
298
|
+
// whole bundle file, breaking the entire app on every real production
|
|
299
|
+
// site with this pattern. Found deploying lumenjs.com's own real build.
|
|
300
|
+
// "Widgets" (2026-09-18): a third permanent root, sibling of Global/View,
|
|
301
|
+
// generalizing V1's hardcoded hasNav/hasHeader/hasFooter widget-loading past
|
|
302
|
+
// its fixed 3-name list — see renderView()'s layout branch below for the
|
|
303
|
+
// resolve/mount/diff logic, and fcs.js's annotateLayoutRegions() for how a
|
|
304
|
+
// layout's own bare attributes (e.g. `<nav nav>`) become the region names
|
|
305
|
+
// used to key this object. Deliberately NOT an ancestor of View in the
|
|
306
|
+
// scope tree (widget regions and [body] are DOM SIBLINGS under the layout
|
|
307
|
+
// root, never one containing the other — unlike a subview's real, singular
|
|
308
|
+
// parent, there's no single unambiguous "the" widget a view could shadow
|
|
309
|
+
// into) — each entry gets its own real `._re` instead (see
|
|
310
|
+
// _dispatchVarsUpdate's generic owner-walk below, which already handles
|
|
311
|
+
// this with zero special-casing once an entry has `._re` set, same as any
|
|
312
|
+
// subview). Starts empty; populated lazily, one entry per declared region
|
|
313
|
+
// name, the first time a layout using that region is mounted.
|
|
243
314
|
let _vt = _x({
|
|
244
315
|
"View": new _v({}),
|
|
245
|
-
"Global": { "vars": {} }
|
|
316
|
+
"Global": { "vars": {}, "fns": {} },
|
|
317
|
+
"Widgets": {}
|
|
246
318
|
});
|
|
247
319
|
|
|
320
|
+
// 2026-09-18: shared by getVal()/evalExp()/concatVarsAtLevel() below, so the
|
|
321
|
+
// "first-declared widget wins on a name collision" rule is defined exactly
|
|
322
|
+
// once. Object key iteration order for string keys is real insertion order,
|
|
323
|
+
// so this naturally checks widgets in the order they were first mounted —
|
|
324
|
+
// which, in practice, is document order (renderView()'s layout branch walks
|
|
325
|
+
// `hstL.regions`, itself built in document order by fcs.js's
|
|
326
|
+
// annotateLayoutRegions()).
|
|
327
|
+
function _lookupInWidgets(name) {
|
|
328
|
+
for (const wname in _vt.Widgets) {
|
|
329
|
+
if (_vt.Widgets[wname].vars.hasOwnProperty(name)) return _vt.Widgets[wname].vars[name];
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// concatVarsAtLevel() below merges via object spread, not the loop-with-
|
|
335
|
+
// break shape _lookupInWidgets() uses — spread's "later wins" needs the
|
|
336
|
+
// widgets merged in REVERSE declaration order so the first-declared widget
|
|
337
|
+
// still wins a name collision, matching _lookupInWidgets()'s own semantics
|
|
338
|
+
// exactly (both must agree, since getVal()/evalExp() use one and
|
|
339
|
+
// concatVarsAtLevel()/lookup()/scopedEval() use the other for the same read).
|
|
340
|
+
function _mergedWidgetsVars() {
|
|
341
|
+
let out = {};
|
|
342
|
+
let names = Object.keys(_vt.Widgets).reverse();
|
|
343
|
+
for (const wname of names) {
|
|
344
|
+
out = { ...out, ..._vt.Widgets[wname].vars };
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
248
349
|
class _lm {
|
|
249
350
|
_RealDOM = [];
|
|
250
351
|
_effects = {};
|
|
@@ -388,7 +489,6 @@ class _lm {
|
|
|
388
489
|
} else {
|
|
389
490
|
effect.nd.setAttribute(effect.name, this.render(effect));
|
|
390
491
|
if (effect.name == "view") {
|
|
391
|
-
cl("We Must Trigger View Change", effect, this.render(effect));
|
|
392
492
|
effect.nd.subPath = this.render(effect);
|
|
393
493
|
renderView(this.render(effect), true, {});
|
|
394
494
|
}
|
|
@@ -407,6 +507,19 @@ class _lm {
|
|
|
407
507
|
this.updateCXRs();
|
|
408
508
|
this.updateLXRs();
|
|
409
509
|
this.updateVXRs();
|
|
510
|
+
|
|
511
|
+
// 2026-09-18: same Reactor({tick}) fix as update() below — also
|
|
512
|
+
// fired on the very first render (not just subsequent reactive
|
|
513
|
+
// updates), since a real tick() use (e.g. hljs.highlightAll())
|
|
514
|
+
// needs to run on initial content too, not just after the first
|
|
515
|
+
// change.
|
|
516
|
+
if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
|
|
517
|
+
try {
|
|
518
|
+
appSettings.tick();
|
|
519
|
+
} catch (e) {
|
|
520
|
+
cl(e);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
410
523
|
}
|
|
411
524
|
|
|
412
525
|
chainConnected(cx) {
|
|
@@ -423,13 +536,25 @@ class _lm {
|
|
|
423
536
|
let subsNames = [];
|
|
424
537
|
for (let i = 0; i < this.view.views.length; i++) {
|
|
425
538
|
const _view = this.view.views[i];
|
|
426
|
-
cl("We Must Render A SubView", _view, subsNames.includes(_view.subPath), subsNames, _view.subPath);
|
|
427
539
|
if (!subsNames.includes(_view.subPath)) subsNames.push(_view.subPath);
|
|
428
540
|
}
|
|
429
541
|
// cl(subsNames);
|
|
542
|
+
// 2026-09-17: real bug found and fixed — this used to always call
|
|
543
|
+
// the plain, no-args renderView(n, true, {}), which only ever
|
|
544
|
+
// searches _vt.View.views (the MAIN view's own array). That's
|
|
545
|
+
// correct when `this` IS the main view, but updateVXRs() also
|
|
546
|
+
// runs on every SUBVIEW instance's own _re (renderAll() calls it
|
|
547
|
+
// unconditionally) — for a subview that itself hosts further
|
|
548
|
+
// subviews, `this.view.views` holds THOSE, not the main view's,
|
|
549
|
+
// so the old call could never find them: nested subviews never
|
|
550
|
+
// rendered at all. Passing this.view.views + this.view.scopePath
|
|
551
|
+
// (set once, in renderHST(), when this instance itself was
|
|
552
|
+
// rendered — ["View"] for the main view, unchanged) lets
|
|
553
|
+
// renderView() search the RIGHT array and address the RIGHT
|
|
554
|
+
// nested scope regardless of how deep this instance is.
|
|
430
555
|
for (let i = 0; i < subsNames.length; i++) {
|
|
431
556
|
const n = subsNames[i];
|
|
432
|
-
renderView(n, true, {});
|
|
557
|
+
renderView(n, true, {}, 'views', this.view.views, this.view.scopePath || ["View"]);
|
|
433
558
|
}
|
|
434
559
|
}
|
|
435
560
|
|
|
@@ -531,7 +656,6 @@ class _lm {
|
|
|
531
656
|
} else {
|
|
532
657
|
effect.nd.setAttribute(effect.name, this.render(effect));
|
|
533
658
|
if (effect.name == "view") {
|
|
534
|
-
cl("We Must Trigger View Change", effect, this.render(effect));
|
|
535
659
|
effect.nd.subPath = this.render(effect);
|
|
536
660
|
renderView(this.render(effect), true, {});
|
|
537
661
|
}
|
|
@@ -551,6 +675,24 @@ class _lm {
|
|
|
551
675
|
const sbscr = this.sbscrbs[sbscsi];
|
|
552
676
|
sbscr._re.update(k);
|
|
553
677
|
}
|
|
678
|
+
|
|
679
|
+
// 2026-09-18: Reactor({tick}) — documented ("runs after every
|
|
680
|
+
// re-render, anywhere", lumenjs-spec.md §5.4/llms.md §3) but never
|
|
681
|
+
// actually called anywhere in this runtime — see this fix's fuller
|
|
682
|
+
// comment at Reactor()'s own definition (vendor/reconnecting-
|
|
683
|
+
// websocket.js) for how this was found. update() is the real
|
|
684
|
+
// per-reactive-change render point (unlike renderAll(), which only
|
|
685
|
+
// ever runs once per _lm instance, at initial mount) — this is
|
|
686
|
+
// where "after every re-render, anywhere" actually lives. Guarded
|
|
687
|
+
// on appSettings existing since update() can fire during index.js's
|
|
688
|
+
// own bootstrap, before Reactor() has run.
|
|
689
|
+
if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
|
|
690
|
+
try {
|
|
691
|
+
appSettings.tick();
|
|
692
|
+
} catch (e) {
|
|
693
|
+
cl(e);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
554
696
|
}
|
|
555
697
|
|
|
556
698
|
async updateLXRs(k) {
|
|
@@ -632,12 +774,61 @@ class _lm {
|
|
|
632
774
|
vx['value'] = marray[index]['value'];
|
|
633
775
|
} else {
|
|
634
776
|
if (forX['as']['k']) vx[forX['as']['k']] = marray[index];
|
|
777
|
+
// 2026-09-17, real bug found and fixed: a bare
|
|
778
|
+
// `:for="items"` (no `as item` alias) is
|
|
779
|
+
// documented — and was V1's real, working
|
|
780
|
+
// behavior — to spread each item's own object
|
|
781
|
+
// keys directly into loop scope, so
|
|
782
|
+
// `items = [{number:1}]` lets a mustache read
|
|
783
|
+
// `{{number}}` with no alias at all. Nothing in
|
|
784
|
+
// this branch ever did that spread — `vx` had
|
|
785
|
+
// no alias key AND no spread keys, so a bare
|
|
786
|
+
// mustache like this always evaluated as an
|
|
787
|
+
// undefined identifier and silently rendered
|
|
788
|
+
// blank. Only spread for a plain-object item —
|
|
789
|
+
// a primitive (string/number) array item has no
|
|
790
|
+
// keys to spread and is read via the untouched
|
|
791
|
+
// alias path instead.
|
|
792
|
+
else if (marray[index] && typeof marray[index] === 'object') {
|
|
793
|
+
for (const k2 in marray[index]) {
|
|
794
|
+
if (Object.prototype.hasOwnProperty.call(marray[index], k2)) {
|
|
795
|
+
vx[k2] = marray[index][k2];
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
635
799
|
}
|
|
636
800
|
// cl(forX, vx);
|
|
637
801
|
// cl("VXXXX", vx, this);
|
|
638
802
|
|
|
639
803
|
if (forIf) {
|
|
640
|
-
|
|
804
|
+
// 2026-09-17, real bug found and fixed:
|
|
805
|
+
// evalExp()'s 2nd param is a NAMES array to
|
|
806
|
+
// look up in _vt.View.vars/_vt.Global.vars (see
|
|
807
|
+
// its other real call site, `this.evalExp(cx.content,
|
|
808
|
+
// this.reactiveVariables)`) — but `vx` here is
|
|
809
|
+
// a VALUES object (the just-built loop-item
|
|
810
|
+
// scope), not a names array. `vars.length` on a
|
|
811
|
+
// plain object is undefined, so evalExp's own
|
|
812
|
+
// `for (i=0; i<vars.length; i++)` loop never
|
|
813
|
+
// ran even once — every `:for-if` condition
|
|
814
|
+
// referencing the loop item (`item.value > 10`,
|
|
815
|
+
// the exact documented example) always
|
|
816
|
+
// evaluated against an empty scope and either
|
|
817
|
+
// threw (caught, filtered out) or read
|
|
818
|
+
// `undefined`. evalExp() already has a real,
|
|
819
|
+
// working mechanism for exactly this — it
|
|
820
|
+
// merges `this.vrs` (the loop-scope override
|
|
821
|
+
// object) at the highest priority — so route
|
|
822
|
+
// through that instead of a second, broken
|
|
823
|
+
// parameter shape.
|
|
824
|
+
let _prevVrs = this.vrs;
|
|
825
|
+
this.vrs = vx;
|
|
826
|
+
let isTrue;
|
|
827
|
+
try {
|
|
828
|
+
isTrue = this.evalExp(forIf, []);
|
|
829
|
+
} finally {
|
|
830
|
+
this.vrs = _prevVrs;
|
|
831
|
+
}
|
|
641
832
|
if (!isTrue) continue;
|
|
642
833
|
}
|
|
643
834
|
|
|
@@ -670,7 +861,29 @@ class _lm {
|
|
|
670
861
|
|
|
671
862
|
// cl("ATR", arrayToRender);
|
|
672
863
|
let oldATR = cx.atr;
|
|
673
|
-
cx.atr
|
|
864
|
+
// 2026-09-17, real bug found and fixed: cx.atr used to
|
|
865
|
+
// store a reference to arrayToRender's own item objects,
|
|
866
|
+
// not an independent snapshot — arrayToRender's items are
|
|
867
|
+
// the actual current elements of the reactive array (e.g.
|
|
868
|
+
// _vt.View.vars["features"]), so a completely ordinary
|
|
869
|
+
// update pattern (mutate an item in place inside .map(),
|
|
870
|
+
// return the same reference — `f.done = !f.done; return
|
|
871
|
+
// f;`, not "rebuild a new object") left oldATR and the
|
|
872
|
+
// next render's arrayToRender pointing at the exact same,
|
|
873
|
+
// already-mutated objects by the time compareArrays()
|
|
874
|
+
// ran. deepCompare()'s JSON.stringify comparison was
|
|
875
|
+
// never wrong — it was comparing an object against
|
|
876
|
+
// itself, so every 'update' action was silently lost and
|
|
877
|
+
// the DOM never re-rendered. clone() (already used a few
|
|
878
|
+
// lines above for `vals`) gives cx.atr a true independent
|
|
879
|
+
// snapshot instead. Found clicking a real @click-toggled
|
|
880
|
+
// :for item on lumenjs.com's own real V2 homepage — no
|
|
881
|
+
// existing test used an in-place item mutation, only
|
|
882
|
+
// whole-array reassignment with new items (see render-
|
|
883
|
+
// hooks.integration.test.js's `images = [...,'c']`),
|
|
884
|
+
// which never exposed this since a length change always
|
|
885
|
+
// hits compareArrays()'s add/remove branches regardless.
|
|
886
|
+
cx.atr = clone(arrayToRender);
|
|
674
887
|
// cl(oldATR, arrayToRender);
|
|
675
888
|
|
|
676
889
|
const actions = this.compareArrays(oldATR, arrayToRender);
|
|
@@ -914,16 +1127,38 @@ class _lm {
|
|
|
914
1127
|
// lookup()/scopedEval(); getVal() (what mustache text nodes
|
|
915
1128
|
// actually call) had its own separate, unmerged read here
|
|
916
1129
|
// and was silently rendering these as empty.
|
|
1130
|
+
// 2026-09-18: inserted a middle tier — _lookupInWidgets()
|
|
1131
|
+
// (all currently-mounted widgets' vars, first-declared
|
|
1132
|
+
// wins) — between View and Global, per this session's
|
|
1133
|
+
// agreed read-fallback order.
|
|
917
1134
|
let __name = this.reactiveVariables[i];
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1135
|
+
if (_vt.View.vars.hasOwnProperty(__name)) {
|
|
1136
|
+
vars[__name] = _vt.View.vars[__name];
|
|
1137
|
+
} else {
|
|
1138
|
+
let _wv = _lookupInWidgets(__name);
|
|
1139
|
+
vars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
|
|
1140
|
+
}
|
|
921
1141
|
}
|
|
922
1142
|
for (const ky in this.vrs) {
|
|
923
1143
|
if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
|
|
924
1144
|
vars[ky] = this.vrs[ky]
|
|
925
1145
|
}
|
|
926
1146
|
}
|
|
1147
|
+
// 2026-09-17: per-instance subview scoping — a subview's own
|
|
1148
|
+
// declared vars live on `this.view.vars` (wired up in
|
|
1149
|
+
// renderHST()/renderView() to be the SAME object its compiled
|
|
1150
|
+
// script's `_vt.View.views[i].vars[...]` writes reach), not
|
|
1151
|
+
// in the flat `_vt.View.vars` checked above (that's still
|
|
1152
|
+
// only ever the MAIN view). Checked last/highest-priority,
|
|
1153
|
+
// same shadowing shape as the `this.vrs` merge just above —
|
|
1154
|
+
// an instance's own var wins over anything it inherited.
|
|
1155
|
+
if (this.view && this.view.vars) {
|
|
1156
|
+
for (const ky in this.view.vars) {
|
|
1157
|
+
if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
|
|
1158
|
+
vars[ky] = this.view.vars[ky];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
927
1162
|
} catch (e) {
|
|
928
1163
|
cl(e);
|
|
929
1164
|
}
|
|
@@ -1033,8 +1268,10 @@ class _lm {
|
|
|
1033
1268
|
// Global vars (index.js's top-level state, see _vt.Global
|
|
1034
1269
|
// above) sit at the lowest priority here — a view or a
|
|
1035
1270
|
// narrower scope declaring the same name shadows it, same
|
|
1036
|
-
// as real JS scoping would.
|
|
1037
|
-
|
|
1271
|
+
// as real JS scoping would. 2026-09-18: widgets' vars sit
|
|
1272
|
+
// one tier above Global, below the view's own — see
|
|
1273
|
+
// _mergedWidgetsVars()'s comment for the ordering rationale.
|
|
1274
|
+
concatenatedVars = { ..._vt.Global.vars, ..._mergedWidgetsVars(), ...parent.view.vars, ...concatenatedVars };
|
|
1038
1275
|
}
|
|
1039
1276
|
return concatenatedVars;
|
|
1040
1277
|
}
|
|
@@ -1157,16 +1394,97 @@ class _lm {
|
|
|
1157
1394
|
createEl(tag, attrs, children, events, doc) {
|
|
1158
1395
|
const _el = document.createElement(tag);
|
|
1159
1396
|
|
|
1397
|
+
// 2026-09-17: every element knows which _lm instance rendered it —
|
|
1398
|
+
// needed so a click on it can resolve its @click handler against
|
|
1399
|
+
// the RIGHT instance's own .fns (see lstnrs.js's evalEvAttr), and
|
|
1400
|
+
// (below) so an @init handler on a subview mount resolves against
|
|
1401
|
+
// ITS creator's own functions/scope, not window. Stamped before
|
|
1402
|
+
// anything below that might need it. Non-enumerable: real bug
|
|
1403
|
+
// found and fixed — an ordinary DOM element has no own enumerable
|
|
1404
|
+
// properties by default, so JSON.stringify(someElement) safely
|
|
1405
|
+
// produced "{}" everywhere this codebase already did that (e.g. a
|
|
1406
|
+
// real test asserting on JSON.stringify(view._slots), real DOM
|
|
1407
|
+
// nodes). An ENUMERABLE back-reference here closes a genuine
|
|
1408
|
+
// cycle (element -> _ownerRe -> that instance's own _RealDOM ->
|
|
1409
|
+
// the same element) and breaks every one of those call sites. Defined this way
|
|
1410
|
+
// instead of a plain `=` assignment so it's reachable
|
|
1411
|
+
// (`el._ownerRe`) but invisible to JSON.stringify/for-in/
|
|
1412
|
+
// Object.keys, matching how a parent-pointer is normally done.
|
|
1413
|
+
Object.defineProperty(_el, '_ownerRe', { value: this, enumerable: false, configurable: true, writable: true });
|
|
1414
|
+
|
|
1160
1415
|
_el.isSub = false;
|
|
1161
1416
|
if (attrs.hasOwnProperty('view')) {
|
|
1162
1417
|
_el.isSub = true;
|
|
1163
1418
|
_el.subPath = attrs['view'];
|
|
1419
|
+
// 2026-09-17: per-instance subview scoping. `views[]` used to
|
|
1420
|
+
// hold just the raw element — every instance of the same
|
|
1421
|
+
// subview file shared one flat _vt.View.vars, so two mounts of
|
|
1422
|
+
// the same .view file collided on the same variable slots (a
|
|
1423
|
+
// real, confirmed bug — clicking one instance changed a value
|
|
1424
|
+
// neither instance's DOM reflected). DOM elements already
|
|
1425
|
+
// carry arbitrary extra JS properties throughout this file
|
|
1426
|
+
// (.isSub, .subPath, .events) — .vars/.views here follow that
|
|
1427
|
+
// same convention rather than a new parallel structure. .vars
|
|
1428
|
+
// is this instance's own scope, addressed at
|
|
1429
|
+
// _vt.View.views[i].vars once renderHST() wires up the path
|
|
1430
|
+
// (see renderView()); .views lets this instance itself host
|
|
1431
|
+
// further nested subviews the same way.
|
|
1432
|
+
_el.vars = {};
|
|
1433
|
+
_el.views = [];
|
|
1434
|
+
_el.fns = {};
|
|
1435
|
+
// 2026-09-17: `@init="fn"` — the actual, decided mechanism
|
|
1436
|
+
// for giving a subview mount its own data at creation time
|
|
1437
|
+
// (replaces an earlier, narrower `:data="name"` attempt —
|
|
1438
|
+
// reusing the existing @before-render/@after-render hook
|
|
1439
|
+
// machinery instead of a bespoke attribute, per real
|
|
1440
|
+
// discussion of real use cases: a subview file mounted
|
|
1441
|
+
// multiple times needing distinct config per instance — the
|
|
1442
|
+
// same widget showing a different crypto coin per mount, or
|
|
1443
|
+
// a :for item needing its own data slice). `fn`'s return
|
|
1444
|
+
// value (a plain object) is merged into this instance's own
|
|
1445
|
+
// `.vars`, so it can hand over multiple named values at once
|
|
1446
|
+
// — not tied to matching a parent variable's name the way
|
|
1447
|
+
// `:data` was. Reads an ancestor's own variable by name
|
|
1448
|
+
// still needs no special syntax at all (getVal()/evalExp()'s
|
|
1449
|
+
// existing fallthrough already covers that); @init is for
|
|
1450
|
+
// data that's computed/selected specifically for this one
|
|
1451
|
+
// mount. fireRenderHook() needed a real fix first (used to
|
|
1452
|
+
// discard its handler's return value entirely) — see its own
|
|
1453
|
+
// comment. Deliberately one-way — a subview writing back to
|
|
1454
|
+
// a PARENT variable (a steps-form wizard sharing accumulated
|
|
1455
|
+
// state across steps, for instance) is a real, different,
|
|
1456
|
+
// not-yet-designed mechanism, not what this covers.
|
|
1457
|
+
//
|
|
1458
|
+
// Calls evalEvAttr() directly rather than fireRenderHook()
|
|
1459
|
+
// (which every other @before-render/@after-render call site
|
|
1460
|
+
// uses): createEl() itself isn't async, and this result has
|
|
1461
|
+
// to be ready synchronously, before the subview actually
|
|
1462
|
+
// renders (a later, separate, real async call). An @init
|
|
1463
|
+
// handler that returns a Promise can't be awaited here —
|
|
1464
|
+
// deliberately unsupported for now, not silently broken:
|
|
1465
|
+
// only a plain object return is used.
|
|
1466
|
+
if (doc && doc.evs && doc.evs.hasOwnProperty('@init')) {
|
|
1467
|
+
let _initAttr = doc.evs['@init'];
|
|
1468
|
+
if (_initAttr) {
|
|
1469
|
+
// 3rd arg: the CALLING scope's own loop-local bindings
|
|
1470
|
+
// (e.g. `item` from `:for="items as item"`) — these
|
|
1471
|
+
// aren't real JS variables an @init handler could
|
|
1472
|
+
// otherwise close over (it's an ordinary top-level
|
|
1473
|
+
// function, not defined inside the loop), so without
|
|
1474
|
+
// this there'd be no way for it to know which
|
|
1475
|
+
// iteration it's being called for at all.
|
|
1476
|
+
let _initResult = evalEvAttr(_initAttr, { cType: 'init' }, $(_el), 'init', this.vrs);
|
|
1477
|
+
if (_initResult && typeof _initResult === 'object' && typeof _initResult.then !== 'function') {
|
|
1478
|
+
Object.assign(_el.vars, _initResult);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1164
1482
|
this.view.views.push(_el);
|
|
1165
1483
|
}
|
|
1166
1484
|
|
|
1167
1485
|
_el.events = {};
|
|
1168
1486
|
for (const prop in attrs) {
|
|
1169
|
-
if (prop == "view" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
|
|
1487
|
+
if (prop == "view" || prop == ":data" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
|
|
1170
1488
|
try {
|
|
1171
1489
|
// cl("DD", doc);
|
|
1172
1490
|
let val = (doc && doc.ax.hasOwnProperty(prop)) ? "" : attrs[prop];
|
|
@@ -1265,10 +1583,48 @@ class _lm {
|
|
|
1265
1583
|
// value scopedEval() would otherwise have found in
|
|
1266
1584
|
// _vt.Global.vars — breaking :if/:for conditions on any
|
|
1267
1585
|
// index.js-declared var, not just mustache text (getVal()).
|
|
1586
|
+
// 2026-09-18: same middle tier as getVal() — see its
|
|
1587
|
+
// matching comment.
|
|
1268
1588
|
let __name = vars[i];
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1589
|
+
if (_vt.View.vars.hasOwnProperty(__name)) {
|
|
1590
|
+
rvars[__name] = _vt.View.vars[__name];
|
|
1591
|
+
} else {
|
|
1592
|
+
let _wv = _lookupInWidgets(__name);
|
|
1593
|
+
rvars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
// 2026-09-17, real bug found and fixed: getVal() (mustache
|
|
1597
|
+
// text, e.g. {{f.name}}) has always merged this.vrs in after
|
|
1598
|
+
// the View/Global loop above — the local :for-loop-scoped
|
|
1599
|
+
// binding (f, k, v, index — set via tx._re.vrs = vx, see the
|
|
1600
|
+
// :for update path above) correctly shadows/supplies names a
|
|
1601
|
+
// view's own script never declares. evalExp() (used for
|
|
1602
|
+
// :if="f.done"/:else-if conditions) never had the same merge
|
|
1603
|
+
// — any :if/:else-if condition referencing a :for loop
|
|
1604
|
+
// variable's property could never evaluate correctly, in any
|
|
1605
|
+
// LumenJS V2 project, not just this page. It "looked" correct
|
|
1606
|
+
// on first render only by coincidence: :else isn't gated by
|
|
1607
|
+
// evalExp at all, so an all-false-by-default loop (every item
|
|
1608
|
+
// starting unchecked) rendered the same whether the condition
|
|
1609
|
+
// genuinely evaluated false or silently threw and was caught
|
|
1610
|
+
// below. Found clicking a real @click-toggled :for item on
|
|
1611
|
+
// lumenjs.com's own real V2 homepage.
|
|
1612
|
+
for (const ky in this.vrs) {
|
|
1613
|
+
if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
|
|
1614
|
+
rvars[ky] = this.vrs[ky];
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// 2026-09-17: same per-instance subview merge as getVal()
|
|
1618
|
+
// above — a subview's own vars live on this.view.vars, not
|
|
1619
|
+
// the flat _vt.View.vars checked above (always the main
|
|
1620
|
+
// view). See getVal()'s matching comment for the full
|
|
1621
|
+
// rationale.
|
|
1622
|
+
if (this.view && this.view.vars) {
|
|
1623
|
+
for (const ky in this.view.vars) {
|
|
1624
|
+
if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
|
|
1625
|
+
rvars[ky] = this.view.vars[ky];
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1272
1628
|
}
|
|
1273
1629
|
} catch (e) {
|
|
1274
1630
|
cl(e);
|
|
@@ -1615,6 +1971,22 @@ class _lm {
|
|
|
1615
1971
|
}
|
|
1616
1972
|
(par.view._slots ?? (par.view._slots = {}))[slotName] = childs;
|
|
1617
1973
|
}
|
|
1974
|
+
} else if (doc.attrs && doc.attrs.hasOwnProperty('tpl') && !doc.attrs.hasOwnProperty(':for')) {
|
|
1975
|
+
// 2026-09-17: `tpl="name"` used WITHOUT `:for=` on the
|
|
1976
|
+
// same element — the real, documented (lumenjs-
|
|
1977
|
+
// spec.md §4.3) shape always pairs the two, and only
|
|
1978
|
+
// makes sense paired: a template renders inside
|
|
1979
|
+
// whatever repeated context it's used from (see
|
|
1980
|
+
// renderSection()'s own tpl handling, for the
|
|
1981
|
+
// WITH-:for case — an element carrying both never
|
|
1982
|
+
// reaches this branch at all, since :for makes it a
|
|
1983
|
+
// 'sections'-type node the switch above already
|
|
1984
|
+
// dispatched elsewhere). Reported as a real error
|
|
1985
|
+
// rather than silently rendering nothing.
|
|
1986
|
+
reportLumenError({
|
|
1987
|
+
stage: 'tpl',
|
|
1988
|
+
error: new Error('tpl="' + doc.attrs['tpl'] + '" must be used together with :for on the same element — templates only render inside a repeated/list context.'),
|
|
1989
|
+
});
|
|
1618
1990
|
} else {
|
|
1619
1991
|
el = par.createEl(doc.name, doc.attrs, [], doc.evs, doc);
|
|
1620
1992
|
|
|
@@ -1797,7 +2169,26 @@ class _lm {
|
|
|
1797
2169
|
}
|
|
1798
2170
|
|
|
1799
2171
|
|
|
1800
|
-
|
|
2172
|
+
// 2026-09-17: `scopePath` (new) — where in the _vt tree THIS render's own
|
|
2173
|
+
// compiled <script> should write/read its reactive vars. Defaults to
|
|
2174
|
+
// ["View"] (the main view, unchanged behavior for every existing caller).
|
|
2175
|
+
// A subview mount passes its own ["View","views",i] (or the nested form
|
|
2176
|
+
// for a subview hosting a subview) — see renderView()'s isSub branch.
|
|
2177
|
+
// `ownVars`/`ownFns`/`ownViews` (new): for a subview render, the SAME
|
|
2178
|
+
// objects the mount element's `.vars`/`.fns`/`.views` already point to
|
|
2179
|
+
// (not fresh `{}`/`[]`s) — so a write through the compiled script's
|
|
2180
|
+
// `_vt.View.views[i].vars[...]`/`.fns[...]` path (which resolves to that
|
|
2181
|
+
// element's `.vars`/`.fns`) and a read through this instance's own
|
|
2182
|
+
// `getVal()`/`evalExp()` (`this.view.vars`) or `evalEvAttr()`
|
|
2183
|
+
// (`this.view.fns`) see the exact same data. `ownViews` matters for
|
|
2184
|
+
// NESTED subviews specifically: createEl() pushes onto `this.view.views`
|
|
2185
|
+
// while THIS instance is rendering — without linking that to the mount
|
|
2186
|
+
// element's own `.views`, a subview hosting further subviews pushed them
|
|
2187
|
+
// into a freshly-constructed, orphaned array nothing else could ever
|
|
2188
|
+
// find (real bug found testing exactly this — nested subviews mounted
|
|
2189
|
+
// and rendered their own content correctly, but their OWN vars/state
|
|
2190
|
+
// were unreachable from outside, landing in a throwaway array).
|
|
2191
|
+
async function renderHST(hst, n, type = 'main', tx, _pv = null, scopePath = ["View"], ownVars, ownFns, ownViews) {
|
|
1801
2192
|
|
|
1802
2193
|
var reactiveVariables = hst.reactiveVars;
|
|
1803
2194
|
hst = hst.hst;
|
|
@@ -1806,10 +2197,16 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
|
|
|
1806
2197
|
"name": n,
|
|
1807
2198
|
"type": type,
|
|
1808
2199
|
"hst": hst,
|
|
1809
|
-
"vars": tx?.vx,
|
|
2200
|
+
"vars": ownVars ?? tx?.vx,
|
|
2201
|
+
"fns": ownFns,
|
|
2202
|
+
"views": ownViews,
|
|
1810
2203
|
"rvs": reactiveVariables,
|
|
1811
2204
|
"_pv": _pv
|
|
1812
2205
|
}));
|
|
2206
|
+
// 2026-09-17: persisted so THIS instance can later render ITS OWN
|
|
2207
|
+
// subviews (updateVXRs() -> renderView()) at the right nested path —
|
|
2208
|
+
// see both of those for the other half of this.
|
|
2209
|
+
_re.view.scopePath = scopePath;
|
|
1813
2210
|
|
|
1814
2211
|
if (Object.keys(_re._cc).length) {
|
|
1815
2212
|
|
|
@@ -1882,7 +2279,7 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
|
|
|
1882
2279
|
const jses = _re._jj[ky];
|
|
1883
2280
|
for (let inde = 0; inde < jses.length; inde++) {
|
|
1884
2281
|
const nd = jses[inde].nd;
|
|
1885
|
-
let code = getWatcher(nd._jst, _re.view, reactiveVariables).code;
|
|
2282
|
+
let code = getWatcher(nd._jst, _re.view, reactiveVariables, scopePath).code;
|
|
1886
2283
|
code = `try { ` + code + ` } catch (e) { reportLumenError({ stage: 'script', error: e }); }`;
|
|
1887
2284
|
// sourceURL: DevTools shows this <script>'s errors against
|
|
1888
2285
|
// "<viewname>.view" instead of an anonymous VM context.
|
|
@@ -1936,8 +2333,16 @@ async function fireRenderHook(cx, n, containerEl, extra) {
|
|
|
1936
2333
|
let ev = Object.assign({ cType: n }, extra || {});
|
|
1937
2334
|
let result = evalEvAttr(attrVal, ev, $(containerEl), n);
|
|
1938
2335
|
if (result && typeof result.then === 'function') {
|
|
1939
|
-
|
|
2336
|
+
// 2026-09-17: this used to just await-and-discard. @init (new,
|
|
2337
|
+
// subview mounts) needs the handler's actual return value — its
|
|
2338
|
+
// whole purpose is "compute this instance's own initial data" —
|
|
2339
|
+
// so a Promise result is now awaited AND returned, not thrown
|
|
2340
|
+
// away. Every existing @before-render/@after-render caller
|
|
2341
|
+
// already ignores this function's return value (none of them
|
|
2342
|
+
// needed it), so this is additive, not a behavior change for them.
|
|
2343
|
+
try { return await result; } catch (e) { return undefined; }
|
|
1940
2344
|
}
|
|
2345
|
+
return result;
|
|
1941
2346
|
}
|
|
1942
2347
|
|
|
1943
2348
|
// 2026-09-16: V1's real automatic plugin-init pass — [sl] (Select2),
|
|
@@ -2126,7 +2531,45 @@ async function renderSection(tx, doc, par, k, sectionsData) {
|
|
|
2126
2531
|
|
|
2127
2532
|
var hst = doc.children;
|
|
2128
2533
|
|
|
2129
|
-
|
|
2534
|
+
// 2026-09-17: `tpl="name"` on a :for element (`<li :for="items as
|
|
2535
|
+
// item" tpl="row"></li>`, the real, documented — lumenjs-spec.md
|
|
2536
|
+
// §4.3 — shape; never built in V2 until now). A :for element's own
|
|
2537
|
+
// `doc.children` is empty (there's nothing between its tags in the
|
|
2538
|
+
// source), which is exactly what's rendered per item above — so
|
|
2539
|
+
// this substitutes the template file's own HST as this section's
|
|
2540
|
+
// children instead, walked by the SAME engine instance rendering
|
|
2541
|
+
// everything else here (this section already has its own vx/vrs —
|
|
2542
|
+
// no separate scope, no isolated instance, just this item's content).
|
|
2543
|
+
if (doc.attrs && doc.attrs.hasOwnProperty('tpl')) {
|
|
2544
|
+
let _payload = (typeof _vcD !== 'undefined' && _vcD) || (typeof _vcData !== 'undefined' ? _vcData : undefined);
|
|
2545
|
+
let _tplName = doc.attrs['tpl'];
|
|
2546
|
+
let _tplKey = btoa("src/tpls/" + _tplName + ".tpl");
|
|
2547
|
+
let _tplEntry = _payload && _payload.tpls && _payload.tpls[_tplKey];
|
|
2548
|
+
if (_tplEntry) {
|
|
2549
|
+
hst = _tplEntry.hst;
|
|
2550
|
+
} else {
|
|
2551
|
+
reportLumenError({
|
|
2552
|
+
stage: 'tpl',
|
|
2553
|
+
error: new Error('tpl="' + _tplName + '" — no such file at src/tpls/' + _tplName + '.tpl'),
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// 2026-09-17: real bug found and fixed — a subview mounted INSIDE a
|
|
2559
|
+
// :for item (e.g. <div :for="items as item"><div view="widgets/
|
|
2560
|
+
// card"></div></div>) never worked at all: renderHST() used to always
|
|
2561
|
+
// build a fresh, empty `.views` array for whatever it constructs
|
|
2562
|
+
// (see _v's constructor), so createEl() calls made while rendering
|
|
2563
|
+
// THIS item's own content pushed the subview's mount element onto a
|
|
2564
|
+
// throwaway array nothing else could ever find — renderView()'s
|
|
2565
|
+
// isSub search only ever looks at the ENCLOSING view's real `.views`
|
|
2566
|
+
// (or a nested subview's own, itself wired the same way). Passing
|
|
2567
|
+
// par's own `.view.views`/`.scopePath` through here makes every
|
|
2568
|
+
// loop item share the SAME real registry as its enclosing view, so
|
|
2569
|
+
// subview mounts inside a loop accumulate into one addressable array
|
|
2570
|
+
// (sequential index, in render order) exactly like a static mount —
|
|
2571
|
+
// same mechanism either way, just a dynamically-arriving index.
|
|
2572
|
+
let _re = await renderHST({ hst, mxes: [] }, tx.key, 'section', tx, par, par?.view?.scopePath || ["View"], undefined, undefined, par?.view?.views);
|
|
2130
2573
|
// if(par) {
|
|
2131
2574
|
// _re.reactiveVariables = par.reactiveVariables;
|
|
2132
2575
|
// }
|
|
@@ -2157,11 +2600,18 @@ async function renderSection(tx, doc, par, k, sectionsData) {
|
|
|
2157
2600
|
return _re;
|
|
2158
2601
|
}
|
|
2159
2602
|
|
|
2160
|
-
|
|
2603
|
+
// `viewsArr`/`scopeBase` (2026-09-17): which subview registry to search
|
|
2604
|
+
// and which scope path to address matches under — default to the main
|
|
2605
|
+
// view's own (_vt.View.views / ["View"]) for the top-level, non-recursive
|
|
2606
|
+
// caller (ws.js's real navigation, index-bootstrap.js, etc. — all
|
|
2607
|
+
// unaffected, still work exactly as before). A subview instance rendering
|
|
2608
|
+
// ITS OWN subviews (updateVXRs(), above) passes its own `.views`/
|
|
2609
|
+
// `.scopePath` instead, so nested subviews resolve against the RIGHT
|
|
2610
|
+
// array at the RIGHT nested address instead of always the main view's.
|
|
2611
|
+
async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["View"]) {
|
|
2161
2612
|
let filePath = "src/views/" + n + ".view"
|
|
2162
2613
|
if (type == 'layouts') filePath = "src/layouts/" + n + ".layout";
|
|
2163
2614
|
|
|
2164
|
-
cl(arguments);
|
|
2165
2615
|
let fileKey = btoa(filePath);
|
|
2166
2616
|
if (!isSub) {
|
|
2167
2617
|
View.props = d ?? {};
|
|
@@ -2213,23 +2663,42 @@ async function renderView(n, isSub, d, type = 'views') {
|
|
|
2213
2663
|
// return;
|
|
2214
2664
|
|
|
2215
2665
|
if (isSub) {
|
|
2666
|
+
// 2026-09-17: per-instance subview scoping — each matched
|
|
2667
|
+
// element needs ITS OWN index within the search array (not its
|
|
2668
|
+
// position in this filtered-by-subPath list) so its compiled
|
|
2669
|
+
// script rewrites into `<scopeBase>.views[<that exact index>].vars`,
|
|
2670
|
+
// not the flat `_vt.View.vars` every subview used to collide on.
|
|
2671
|
+
// Searches viewsArr (this main view's _vt.View.views by
|
|
2672
|
+
// default — unchanged for the top-level caller) or, when
|
|
2673
|
+
// called recursively from a subview instance's own
|
|
2674
|
+
// updateVXRs(), THAT instance's own `.views` — see
|
|
2675
|
+
// updateVXRs() for why nested subviews need this.
|
|
2676
|
+
let searchArr = viewsArr || _vt.View.views;
|
|
2216
2677
|
let els = [];
|
|
2217
|
-
for (let i = 0; i <
|
|
2218
|
-
let _el =
|
|
2678
|
+
for (let i = 0; i < searchArr.length; i++) {
|
|
2679
|
+
let _el = searchArr[i];
|
|
2219
2680
|
if (_el.__isProxy) _el = _el.target;
|
|
2220
|
-
if (_el.subPath == n) els.push(_el);
|
|
2681
|
+
if (_el.subPath == n) els.push({ el: _el, viewsIndex: i });
|
|
2221
2682
|
}
|
|
2222
2683
|
if (els.length) {
|
|
2223
2684
|
// cl(els);
|
|
2224
2685
|
for (let i = 0; i < els.length; i++) {
|
|
2225
|
-
|
|
2226
|
-
|
|
2686
|
+
const { el, viewsIndex } = els[i];
|
|
2687
|
+
// Create/reuse el.vars BEFORE rendering, so the fresh
|
|
2688
|
+
// _v instance renderHST() constructs uses this EXACT
|
|
2689
|
+
// object as its own .vars — see renderHST()'s ownVars
|
|
2690
|
+
// param comment for why that identity matters.
|
|
2691
|
+
el.vars = el.vars || {};
|
|
2692
|
+
el.views = el.views || [];
|
|
2693
|
+
el.fns = el.fns || {};
|
|
2694
|
+
let _re = await renderHST(hst, n, 'sub', undefined, null, scopeBase.concat(["views", viewsIndex]), el.vars, el.fns, el.views);
|
|
2695
|
+
el._re = _re;
|
|
2227
2696
|
el.innerHTML = "";
|
|
2228
2697
|
// cl(el);
|
|
2229
2698
|
el.append(..._re._RealDOM);
|
|
2230
2699
|
_re.renderAll();
|
|
2231
2700
|
}
|
|
2232
|
-
// _re._RealDOM.forEach(_el => el[0].appendChild(_el))
|
|
2701
|
+
// _re._RealDOM.forEach(_el => el[0].appendChild(_el))
|
|
2233
2702
|
// cl(_re);
|
|
2234
2703
|
}
|
|
2235
2704
|
} else {
|
|
@@ -2255,12 +2724,21 @@ async function renderView(n, isSub, d, type = 'views') {
|
|
|
2255
2724
|
|
|
2256
2725
|
let appSelector = appSettings.App ?? "[app]";
|
|
2257
2726
|
var appContainer = $(appSelector);
|
|
2727
|
+
// 2026-09-18: whether THIS call just replaced the layout's
|
|
2728
|
+
// own DOM wholesale — needed below by the widget
|
|
2729
|
+
// resolve/mount/diff pass, since a widget whose resolution
|
|
2730
|
+
// hasn't changed still needs its already-rendered nodes
|
|
2731
|
+
// reattached to the fresh layout element (the old one they
|
|
2732
|
+
// were living in is gone), even though nothing about the
|
|
2733
|
+
// widget itself needs re-rendering.
|
|
2734
|
+
let layoutJustSwapped = false;
|
|
2258
2735
|
if (appContainer.length) {
|
|
2259
2736
|
let currentLayout = $(appSelector).data('layout');
|
|
2260
2737
|
if (currentLayout != layout) {
|
|
2261
2738
|
let _rel = await renderHST(hstL, layout, 'layout');
|
|
2262
2739
|
appContainer.data('layout', layout).html(_rel._RealDOM);
|
|
2263
2740
|
_rel.renderAll();
|
|
2741
|
+
layoutJustSwapped = true;
|
|
2264
2742
|
} else {
|
|
2265
2743
|
// cl("Same Layout");
|
|
2266
2744
|
}
|
|
@@ -2270,6 +2748,59 @@ async function renderView(n, isSub, d, type = 'views') {
|
|
|
2270
2748
|
appContainer = $(appSelector);
|
|
2271
2749
|
appContainer.data('layout', layout).html(_rel._RealDOM);
|
|
2272
2750
|
_rel.renderAll();
|
|
2751
|
+
layoutJustSwapped = true;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// Persistent layout widgets (_vt.Widgets, 2026-09-18) — see
|
|
2755
|
+
// that object's own declaration comment for the design.
|
|
2756
|
+
// Runs on EVERY navigation, not gated on layoutJustSwapped:
|
|
2757
|
+
// a view's own settings (hasNav: false, hasNav: "nav2", ...)
|
|
2758
|
+
// can change what belongs in a region even when the layout
|
|
2759
|
+
// itself didn't change at all. hstL.regions is precomputed
|
|
2760
|
+
// build-time by fcs.js's annotateLayoutRegions() — the bare
|
|
2761
|
+
// (valueless) attribute names the CURRENTLY MOUNTED layout
|
|
2762
|
+
// itself declares (e.g. ["header","nav","footer"] for
|
|
2763
|
+
// `<header header>`/`<nav nav>`/`<footer footer>`).
|
|
2764
|
+
let declaredRegions = (hstL && hstL.regions) || [];
|
|
2765
|
+
for (const regionName of declaredRegions) {
|
|
2766
|
+
let settingsKey = "has" + regionName[0].toUpperCase() + regionName.slice(1);
|
|
2767
|
+
let want = _re.view.settings.hasOwnProperty(settingsKey) ? _re.view.settings[settingsKey] : true;
|
|
2768
|
+
let resolvedFile = want === false ? null : (want === true ? regionName : want);
|
|
2769
|
+
|
|
2770
|
+
let w = _vt.Widgets[regionName] || (_vt.Widgets[regionName] = { vars: {}, fns: {}, views: [], _re: null, _resolvedFile: undefined });
|
|
2771
|
+
|
|
2772
|
+
if (resolvedFile !== w._resolvedFile) {
|
|
2773
|
+
// Resolution genuinely changed since the last time
|
|
2774
|
+
// this region was resolved — mount fresh, or clear.
|
|
2775
|
+
if (resolvedFile === null) {
|
|
2776
|
+
$('[' + regionName + ']').html('');
|
|
2777
|
+
w._re = null;
|
|
2778
|
+
} else {
|
|
2779
|
+
let wFilePath = "src/views/widgets/" + resolvedFile + ".view";
|
|
2780
|
+
let wHst = _payload['views'][btoa(wFilePath)];
|
|
2781
|
+
if (wHst) {
|
|
2782
|
+
let _rew = await renderHST(wHst, resolvedFile, 'widget', undefined, null, ["Widgets", regionName], w.vars, w.fns, w.views);
|
|
2783
|
+
w._re = _rew;
|
|
2784
|
+
$('[' + regionName + ']').html(_rew._RealDOM);
|
|
2785
|
+
_rew.renderAll();
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
w._resolvedFile = resolvedFile;
|
|
2789
|
+
} else if (layoutJustSwapped && w._re) {
|
|
2790
|
+
// Same widget as before, but the layout element it
|
|
2791
|
+
// lives in was just replaced wholesale — reattach
|
|
2792
|
+
// the EXISTING rendered nodes (.html() moves real
|
|
2793
|
+
// node references, same as [body]/[slot] below,
|
|
2794
|
+
// it doesn't clone) rather than re-rendering. The
|
|
2795
|
+
// widget's own state (w.vars/.fns/.views) never
|
|
2796
|
+
// lived on the old DOM to begin with, so nothing
|
|
2797
|
+
// was lost — only the attachment point needed
|
|
2798
|
+
// fixing up.
|
|
2799
|
+
$('[' + regionName + ']').html(w._re._RealDOM);
|
|
2800
|
+
}
|
|
2801
|
+
// else: nothing changed and the layout didn't just
|
|
2802
|
+
// swap — this region's DOM/state is left completely
|
|
2803
|
+
// untouched, on purpose (the whole point of this).
|
|
2273
2804
|
}
|
|
2274
2805
|
|
|
2275
2806
|
// return;
|