@jxsuite/runtime 0.28.4 → 0.29.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/runtime.js +195 -45
- package/dist/runtime.js.map +5 -5
- package/package.json +3 -2
- package/src/expression.ts +3 -3
- package/src/runtime.ts +179 -95
- package/src/types.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "bun build ./src/runtime.ts --outdir dist --target browser --sourcemap=linked",
|
|
26
|
+
"build:metafile": "bun build ./src/runtime.ts --outdir dist --target browser --sourcemap=linked --metafile=metafile.json",
|
|
26
27
|
"test": "bun test --isolate",
|
|
27
28
|
"test:coverage": "bun test --isolate --coverage",
|
|
28
29
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@jxsuite/schema": "^0.
|
|
32
|
+
"@jxsuite/schema": "^0.29.0",
|
|
32
33
|
"@vue/reactivity": "^3.5.38"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
package/src/expression.ts
CHANGED
|
@@ -139,7 +139,7 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
|
|
|
139
139
|
} else if (key === "index") {
|
|
140
140
|
base = iterCtx?.index ?? map?.index ?? state["$map/index"];
|
|
141
141
|
} else {
|
|
142
|
-
base = map?.[key] ?? state[`$map/${key}`];
|
|
142
|
+
base = map?.[key!] ?? state[`$map/${key}`];
|
|
143
143
|
}
|
|
144
144
|
return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
|
|
145
145
|
}
|
|
@@ -167,7 +167,7 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
|
|
|
167
167
|
function resolveWritableRef(
|
|
168
168
|
ref: string,
|
|
169
169
|
state: JxScope,
|
|
170
|
-
|
|
170
|
+
_event: Event | null,
|
|
171
171
|
iterCtx?: IterCtx,
|
|
172
172
|
): { obj: JxScope; key: string } {
|
|
173
173
|
if (ref.startsWith("$map/")) {
|
|
@@ -180,7 +180,7 @@ function resolveWritableRef(
|
|
|
180
180
|
} else if (key === "index") {
|
|
181
181
|
base = iterCtx?.index ?? map?.index ?? state["$map/index"];
|
|
182
182
|
} else {
|
|
183
|
-
base = map?.[key] ?? state[`$map/${key}`];
|
|
183
|
+
base = map?.[key!] ?? state[`$map/${key}`];
|
|
184
184
|
}
|
|
185
185
|
if (parts.length > 2) {
|
|
186
186
|
const pathParts = parts.slice(2);
|
package/src/runtime.ts
CHANGED
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
* @module jx
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
computed,
|
|
18
|
+
effect,
|
|
19
|
+
effectScope,
|
|
20
|
+
isRef,
|
|
21
|
+
onEffectCleanup,
|
|
22
|
+
reactive,
|
|
23
|
+
ref,
|
|
24
|
+
} from "@vue/reactivity";
|
|
17
25
|
import { evaluateExpression, isMutating } from "./expression.ts";
|
|
18
26
|
import type { DynamicClass, JxEventHandler, JxPath, JxRenderOptions, JxScope } from "./types.ts";
|
|
19
27
|
import {
|
|
@@ -81,7 +89,7 @@ export async function Jx(
|
|
|
81
89
|
const state = await buildScope(doc, {}, base);
|
|
82
90
|
target.append(renderNode(doc, state, options));
|
|
83
91
|
if (typeof state.onMount === "function") {
|
|
84
|
-
state.onMount(state);
|
|
92
|
+
(state.onMount as (s: JxScope) => unknown)(state);
|
|
85
93
|
}
|
|
86
94
|
return state;
|
|
87
95
|
}
|
|
@@ -300,16 +308,31 @@ export { hasSchemaKeywords };
|
|
|
300
308
|
* @param {JxScope} state
|
|
301
309
|
* @returns {string}
|
|
302
310
|
*/
|
|
303
|
-
function evaluateTemplate(str: string, state: JxScope) {
|
|
311
|
+
function evaluateTemplate(str: string, state: JxScope): string {
|
|
304
312
|
const $map = state?.$map as { item?: unknown; index?: number } | undefined;
|
|
305
|
-
const fn = new Function("state", "$map", "item", "index", `return \`${str}\``)
|
|
313
|
+
const fn = new Function("state", "$map", "item", "index", `return \`${str}\``) as (
|
|
314
|
+
state: JxScope,
|
|
315
|
+
$map: unknown,
|
|
316
|
+
item: unknown,
|
|
317
|
+
index: number | undefined,
|
|
318
|
+
) => string;
|
|
306
319
|
return fn(state, $map, $map?.item, $map?.index);
|
|
307
320
|
}
|
|
308
321
|
|
|
309
322
|
// ─── Step 2b: Function resolution (Shape 4) ─────────────────────────────────
|
|
310
323
|
|
|
324
|
+
/** Shape of a dynamically imported module: named exports plus an optional default. */
|
|
325
|
+
type ImportedModule = Record<string, unknown> & { default?: Record<string, unknown> };
|
|
326
|
+
|
|
327
|
+
/** Minimal contract an externally-imported resolver class may implement. */
|
|
328
|
+
interface ExternalClassInstance {
|
|
329
|
+
value?: unknown;
|
|
330
|
+
resolve?: () => unknown;
|
|
331
|
+
subscribe?: (cb: (newVal: unknown) => void) => void;
|
|
332
|
+
}
|
|
333
|
+
|
|
311
334
|
/** Module cache for $src imports (shared with external class resolution). */
|
|
312
|
-
const _moduleCache = new Map();
|
|
335
|
+
const _moduleCache = new Map<string, ImportedModule>();
|
|
313
336
|
|
|
314
337
|
/**
|
|
315
338
|
* Resolve a $prototype: "Function" entry into a function or computed.
|
|
@@ -337,11 +360,11 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
337
360
|
return noop;
|
|
338
361
|
}
|
|
339
362
|
|
|
340
|
-
let fn;
|
|
363
|
+
let fn: ((...args: unknown[]) => unknown) | undefined;
|
|
341
364
|
|
|
342
365
|
if (def.body) {
|
|
343
366
|
const params = resolveParamNames(def);
|
|
344
|
-
fn = new Function(...params, def.body);
|
|
367
|
+
fn = new Function(...params, def.body) as (...args: unknown[]) => unknown;
|
|
345
368
|
Object.defineProperty(fn, "name", {
|
|
346
369
|
configurable: true,
|
|
347
370
|
value: def.name ?? key,
|
|
@@ -353,26 +376,27 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
353
376
|
throw new Error(`Jx: '${key}' has neither body nor $src`);
|
|
354
377
|
}
|
|
355
378
|
const exportName = def.$export ?? key;
|
|
356
|
-
let mod;
|
|
379
|
+
let mod: ImportedModule;
|
|
357
380
|
if (_moduleCache.has(src)) {
|
|
358
|
-
mod = _moduleCache.get(src)
|
|
381
|
+
mod = _moduleCache.get(src)!;
|
|
359
382
|
} else {
|
|
360
383
|
if (base) {
|
|
361
384
|
const resolvedSrc = new URL(src, base).href;
|
|
362
385
|
try {
|
|
363
|
-
mod = await import(resolvedSrc);
|
|
386
|
+
mod = (await import(resolvedSrc)) as ImportedModule;
|
|
364
387
|
} catch {
|
|
365
|
-
mod = await import(src);
|
|
388
|
+
mod = (await import(src)) as ImportedModule;
|
|
366
389
|
}
|
|
367
390
|
} else {
|
|
368
|
-
mod = await import(src);
|
|
391
|
+
mod = (await import(src)) as ImportedModule;
|
|
369
392
|
}
|
|
370
393
|
_moduleCache.set(src, mod);
|
|
371
394
|
}
|
|
372
|
-
|
|
373
|
-
if (typeof
|
|
395
|
+
const candidate = mod[exportName] ?? mod.default?.[exportName];
|
|
396
|
+
if (typeof candidate !== "function") {
|
|
374
397
|
throw new TypeError(`Jx: export "${exportName}" not found or not a function in "${src}"`);
|
|
375
398
|
}
|
|
399
|
+
fn = candidate as (...args: unknown[]) => unknown;
|
|
376
400
|
}
|
|
377
401
|
|
|
378
402
|
// Detect computed: body contains a return statement, or $src function introspection.
|
|
@@ -475,11 +499,11 @@ export function renderNode(
|
|
|
475
499
|
}
|
|
476
500
|
|
|
477
501
|
// Extend scope with any $-prefixed local bindings declared on this node
|
|
478
|
-
let localState = state;
|
|
502
|
+
let localState: JxScope = state;
|
|
479
503
|
for (const [key, val] of Object.entries(def)) {
|
|
480
504
|
if (key.startsWith("$") && !RESERVED_KEYS.has(key)) {
|
|
481
505
|
if (localState === state) {
|
|
482
|
-
localState = Object.create(state);
|
|
506
|
+
localState = Object.create(state) as JxScope;
|
|
483
507
|
}
|
|
484
508
|
localState[key] = isRefObj(val) ? resolveRef(val.$ref, state) : val;
|
|
485
509
|
}
|
|
@@ -500,14 +524,11 @@ export function renderNode(
|
|
|
500
524
|
if (def.$switch) {
|
|
501
525
|
return renderSwitch(def, localState, options);
|
|
502
526
|
}
|
|
503
|
-
if (isMappedArray(def.children)) {
|
|
504
|
-
return renderMappedArray(def, def.children, localState, options);
|
|
505
|
-
}
|
|
506
527
|
|
|
507
528
|
const el = document.createElement(tagName);
|
|
508
529
|
|
|
509
530
|
if (options?.onNodeCreated) {
|
|
510
|
-
options.onNodeCreated(el, path, def);
|
|
531
|
+
options.onNodeCreated(el, path, def, localState);
|
|
511
532
|
}
|
|
512
533
|
|
|
513
534
|
applyProperties(el, def, localState);
|
|
@@ -519,10 +540,23 @@ export function renderNode(
|
|
|
519
540
|
);
|
|
520
541
|
applyAttributes(el, def.attributes ?? {}, localState);
|
|
521
542
|
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
543
|
+
const kids = def.children;
|
|
544
|
+
if (isMappedArray(kids)) {
|
|
545
|
+
// Legacy whole-children repeater: the items render directly into `el` (which keeps its own
|
|
546
|
+
// TagName, e.g. <ul>), with no extra wrapper element.
|
|
547
|
+
const arrOpts = options ? { ...options, _path: [...path, "children"] } : undefined;
|
|
548
|
+
renderMappedArrayInto(el, kids, localState, arrOpts);
|
|
549
|
+
} else if (Array.isArray(kids)) {
|
|
550
|
+
for (let i = 0; i < kids.length; i++) {
|
|
551
|
+
const child = kids[i]!;
|
|
552
|
+
const childOpts = options ? { ...options, _path: [...path, "children", i] } : undefined;
|
|
553
|
+
if (isMappedArray(child)) {
|
|
554
|
+
// Array pseudo-element among siblings: expand inline, no wrapper.
|
|
555
|
+
renderMappedArrayInto(el, child, localState, childOpts);
|
|
556
|
+
} else {
|
|
557
|
+
el.append(renderNode(child, localState, childOpts));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
526
560
|
}
|
|
527
561
|
|
|
528
562
|
return el;
|
|
@@ -562,14 +596,15 @@ function applyProperties(el: HTMLElement, def: JxElement, state: JxScope) {
|
|
|
562
596
|
const handler = resolveRef(val.$ref, state);
|
|
563
597
|
if (typeof handler === "function") {
|
|
564
598
|
const scope = state;
|
|
565
|
-
|
|
599
|
+
const handlerFn = handler as (s: JxScope, e: Event) => unknown;
|
|
600
|
+
el.addEventListener(key.slice(2), (e) => handlerFn(scope, e));
|
|
566
601
|
}
|
|
567
602
|
continue;
|
|
568
603
|
}
|
|
569
604
|
// Event handler: inline $prototype: "Function"
|
|
570
605
|
if (isFunctionDef(val) && val.body) {
|
|
571
606
|
const params = resolveParamNames(val);
|
|
572
|
-
const fn = new Function(...params, val.body);
|
|
607
|
+
const fn = new Function(...params, val.body) as (s: JxScope, e: Event) => unknown;
|
|
573
608
|
const scope = state;
|
|
574
609
|
el.addEventListener(key.slice(2), (e) => fn(scope, e));
|
|
575
610
|
continue;
|
|
@@ -781,7 +816,40 @@ export function applyStyle(
|
|
|
781
816
|
|
|
782
817
|
const tag = document.createElement("style");
|
|
783
818
|
tag.textContent = css;
|
|
819
|
+
tag.dataset.jxOwner = uid;
|
|
784
820
|
document.head.append(tag);
|
|
821
|
+
elementStyleTags.set(el, tag);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Scoped <style> tags emitted per element by applyStyle (latest wins). Lets callers replace an
|
|
826
|
+
* element's emitted styles instead of accumulating orphaned tags.
|
|
827
|
+
*/
|
|
828
|
+
export const elementStyleTags = new WeakMap<HTMLElement, HTMLStyleElement>();
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Re-apply an element's style definition in place: removes the previously emitted scoped <style>
|
|
832
|
+
* tag (if any), clears inline styles, and applies the new definition.
|
|
833
|
+
*
|
|
834
|
+
* @param {HTMLElement} el
|
|
835
|
+
* @param {JxStyle | undefined} styleDef
|
|
836
|
+
* @param {Record<string, string>} [mediaQueries]
|
|
837
|
+
* @param {JxScope} [state]
|
|
838
|
+
*/
|
|
839
|
+
export function reapplyStyle(
|
|
840
|
+
el: HTMLElement,
|
|
841
|
+
styleDef: JxStyle | undefined,
|
|
842
|
+
mediaQueries: Record<string, string> = {},
|
|
843
|
+
state: JxScope = {},
|
|
844
|
+
) {
|
|
845
|
+
const prev = elementStyleTags.get(el);
|
|
846
|
+
if (prev) {
|
|
847
|
+
prev.remove();
|
|
848
|
+
elementStyleTags.delete(el);
|
|
849
|
+
}
|
|
850
|
+
el.style.cssText = "";
|
|
851
|
+
delete el.dataset.jx;
|
|
852
|
+
applyStyle(el, styleDef ?? {}, mediaQueries, state);
|
|
785
853
|
}
|
|
786
854
|
|
|
787
855
|
/**
|
|
@@ -804,65 +872,72 @@ function applyAttributes(el: HTMLElement, attrs: Record<string, JxAttributeValue
|
|
|
804
872
|
// ─── Array mapping ────────────────────────────────────────────────────────────
|
|
805
873
|
|
|
806
874
|
/**
|
|
807
|
-
*
|
|
875
|
+
* Render a mapped array (repeater) wrapper-less: its item instances are inserted directly into
|
|
876
|
+
* `parentEl`, in place, ahead of an anchor comment that marks the array's position among the
|
|
877
|
+
* parent's other children. Re-renders reactively when `items` (or the filter/sort sources) change;
|
|
878
|
+
* each generation's item renders live in their own detached effect scope so nested arrays and
|
|
879
|
+
* template bindings are disposed — not leaked or double-fired — on the next change.
|
|
880
|
+
*
|
|
881
|
+
* `options._path` is the array node's own document path (`[…, "children", i]`, or `[…, "children"]`
|
|
882
|
+
* for a legacy whole-children repeater); item instances render at `[…that…, "map", index]`.
|
|
883
|
+
*
|
|
884
|
+
* @param {HTMLElement} parentEl
|
|
808
885
|
* @param {import("@jxsuite/schema/types").JxMappedArray} arrayDef
|
|
809
886
|
* @param {JxScope} state
|
|
810
887
|
* @param {JxRenderOptions} [options]
|
|
811
|
-
* @returns {HTMLElement}
|
|
812
888
|
*/
|
|
813
|
-
function
|
|
814
|
-
|
|
889
|
+
function renderMappedArrayInto(
|
|
890
|
+
parentEl: HTMLElement,
|
|
815
891
|
arrayDef: JxMappedArray,
|
|
816
892
|
state: JxScope,
|
|
817
893
|
options?: JxRenderOptions,
|
|
818
894
|
) {
|
|
819
895
|
const path = options?._path ?? [];
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
if (options?.onNodeCreated) {
|
|
823
|
-
options.onNodeCreated(container, path, def);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
applyProperties(container, def, state);
|
|
827
|
-
applyStyle(container, def.style ?? {}, (state["$media"] as Record<string, string>) ?? {}, state);
|
|
828
|
-
applyAttributes(container, def.attributes ?? {}, state);
|
|
896
|
+
const anchor = document.createComment("jx-array");
|
|
897
|
+
parentEl.append(anchor);
|
|
829
898
|
const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef } = arrayDef;
|
|
830
899
|
|
|
831
900
|
effect(() => {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
items = isRefObj(itemsSrc) ? resolveRef(itemsSrc.$ref, state) : itemsSrc;
|
|
835
|
-
if (!Array.isArray(items)) {
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
if (isRefObj(filterRef)) {
|
|
901
|
+
let items: unknown = isRefObj(itemsSrc) ? resolveRef(itemsSrc.$ref, state) : itemsSrc;
|
|
902
|
+
if (Array.isArray(items) && isRefObj(filterRef)) {
|
|
839
903
|
const fn = resolveRef(filterRef.$ref, state);
|
|
840
904
|
if (typeof fn === "function") {
|
|
841
905
|
items = items.filter(fn as (v: unknown) => boolean);
|
|
842
906
|
}
|
|
843
907
|
}
|
|
844
|
-
if (isRefObj(sortRef)) {
|
|
908
|
+
if (Array.isArray(items) && isRefObj(sortRef)) {
|
|
845
909
|
const fn = resolveRef(sortRef.$ref, state);
|
|
846
910
|
if (typeof fn === "function") {
|
|
847
911
|
items = [...(items as unknown[])].toSorted(fn as (a: unknown, b: unknown) => number);
|
|
848
912
|
}
|
|
849
913
|
}
|
|
914
|
+
if (!Array.isArray(items) || !mapDef) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
850
917
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
918
|
+
// Render this generation's items inside a detached scope; the cleanup (run before the next
|
|
919
|
+
// Re-render and when the enclosing render scope stops) tears it down and removes its nodes.
|
|
920
|
+
const scope = effectScope(true);
|
|
921
|
+
const nodes: ChildNode[] = [];
|
|
922
|
+
scope.run(() => {
|
|
923
|
+
for (const [index, item] of (items as unknown[]).entries()) {
|
|
924
|
+
const child = Object.create(state) as JxScope;
|
|
925
|
+
child.$map = { index, item };
|
|
926
|
+
child["$map/item"] = item;
|
|
927
|
+
child["$map/index"] = index;
|
|
928
|
+
const childOpts = options ? { ...options, _path: [...path, "map", index] } : undefined;
|
|
929
|
+
const node = renderNode(mapDef, child, childOpts);
|
|
930
|
+
anchor.before(node);
|
|
931
|
+
nodes.push(node);
|
|
861
932
|
}
|
|
862
|
-
}
|
|
933
|
+
});
|
|
934
|
+
onEffectCleanup(() => {
|
|
935
|
+
scope.stop();
|
|
936
|
+
for (const n of nodes) {
|
|
937
|
+
n.remove();
|
|
938
|
+
}
|
|
939
|
+
});
|
|
863
940
|
});
|
|
864
|
-
|
|
865
|
-
return container;
|
|
866
941
|
}
|
|
867
942
|
|
|
868
943
|
// ─── $switch ──────────────────────────────────────────────────────────────────
|
|
@@ -878,7 +953,7 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
|
|
|
878
953
|
const container = document.createElement(def.tagName ?? "div");
|
|
879
954
|
|
|
880
955
|
if (options?.onNodeCreated) {
|
|
881
|
-
options.onNodeCreated(container, path, def);
|
|
956
|
+
options.onNodeCreated(container, path, def, state);
|
|
882
957
|
}
|
|
883
958
|
|
|
884
959
|
applyProperties(container, def, state);
|
|
@@ -960,7 +1035,7 @@ export async function resolvePrototype(
|
|
|
960
1035
|
|
|
961
1036
|
if (!def.manual) {
|
|
962
1037
|
effect(() => {
|
|
963
|
-
let url;
|
|
1038
|
+
let url: string | undefined;
|
|
964
1039
|
if (isTemplateString(def.url)) {
|
|
965
1040
|
url = evaluateTemplate(def.url, state);
|
|
966
1041
|
} else {
|
|
@@ -1000,7 +1075,7 @@ export async function resolvePrototype(
|
|
|
1000
1075
|
if (debounceMs > 0) {
|
|
1001
1076
|
debounceTimer = setTimeout(doFetch, debounceMs);
|
|
1002
1077
|
} else {
|
|
1003
|
-
doFetch();
|
|
1078
|
+
void doFetch();
|
|
1004
1079
|
}
|
|
1005
1080
|
});
|
|
1006
1081
|
}
|
|
@@ -1013,11 +1088,13 @@ export async function resolvePrototype(
|
|
|
1013
1088
|
const p: Record<string, string> = {};
|
|
1014
1089
|
for (const [k, v] of Object.entries(def)) {
|
|
1015
1090
|
if (k !== "$prototype") {
|
|
1016
|
-
p[k] =
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1091
|
+
p[k] = (
|
|
1092
|
+
isRefObj(v)
|
|
1093
|
+
? resolveRef(v.$ref, state)
|
|
1094
|
+
: isTemplateString(v)
|
|
1095
|
+
? evaluateTemplate(v, state)
|
|
1096
|
+
: v
|
|
1097
|
+
) as string;
|
|
1021
1098
|
}
|
|
1022
1099
|
}
|
|
1023
1100
|
return new URLSearchParams(p).toString();
|
|
@@ -1028,10 +1105,10 @@ export async function resolvePrototype(
|
|
|
1028
1105
|
case "SessionStorage": {
|
|
1029
1106
|
const store = def.$prototype === "LocalStorage" ? localStorage : sessionStorage;
|
|
1030
1107
|
const k = def.key ?? key;
|
|
1031
|
-
let init;
|
|
1108
|
+
let init: unknown;
|
|
1032
1109
|
try {
|
|
1033
1110
|
const s = store.getItem(k);
|
|
1034
|
-
init = s !== null ? JSON.parse(s) : (def.default ?? null);
|
|
1111
|
+
init = s !== null ? (JSON.parse(s) as unknown) : (def.default ?? null);
|
|
1035
1112
|
} catch {
|
|
1036
1113
|
init = def.default ?? null;
|
|
1037
1114
|
}
|
|
@@ -1060,7 +1137,7 @@ export async function resolvePrototype(
|
|
|
1060
1137
|
return null;
|
|
1061
1138
|
}
|
|
1062
1139
|
try {
|
|
1063
|
-
return JSON.parse(decodeURIComponent(m[1]));
|
|
1140
|
+
return JSON.parse(decodeURIComponent(m[1]!));
|
|
1064
1141
|
} catch {
|
|
1065
1142
|
return m[1];
|
|
1066
1143
|
}
|
|
@@ -1221,16 +1298,16 @@ async function resolveExternalPrototype(
|
|
|
1221
1298
|
* @returns {Promise<unknown>}
|
|
1222
1299
|
*/
|
|
1223
1300
|
async function importAndInstantiate(def: JxScope, src: string, exportName: string, base?: string) {
|
|
1224
|
-
let mod;
|
|
1301
|
+
let mod: ImportedModule;
|
|
1225
1302
|
if (_moduleCache.has(src)) {
|
|
1226
|
-
mod = _moduleCache.get(src)
|
|
1303
|
+
mod = _moduleCache.get(src)!;
|
|
1227
1304
|
} else {
|
|
1228
1305
|
try {
|
|
1229
|
-
mod = await import(src);
|
|
1306
|
+
mod = (await import(src)) as ImportedModule;
|
|
1230
1307
|
} catch {
|
|
1231
1308
|
if (base) {
|
|
1232
1309
|
const resolvedSrc = new URL(src, base).href;
|
|
1233
|
-
mod = await import(resolvedSrc);
|
|
1310
|
+
mod = (await import(resolvedSrc)) as ImportedModule;
|
|
1234
1311
|
} else {
|
|
1235
1312
|
throw new Error(`Failed to import "${src}"`);
|
|
1236
1313
|
}
|
|
@@ -1253,9 +1330,10 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
|
|
|
1253
1330
|
}
|
|
1254
1331
|
}
|
|
1255
1332
|
|
|
1256
|
-
const
|
|
1333
|
+
const Ctor = ExportedClass as new (config: JxScope) => ExternalClassInstance;
|
|
1334
|
+
const instance = new Ctor(config);
|
|
1257
1335
|
|
|
1258
|
-
let value;
|
|
1336
|
+
let value: unknown;
|
|
1259
1337
|
if (typeof instance.resolve === "function") {
|
|
1260
1338
|
value = await instance.resolve();
|
|
1261
1339
|
} else if ("value" in instance) {
|
|
@@ -1342,9 +1420,9 @@ async function resolveClassJson(def: JxPrototypeDef, state: JxScope, key: string
|
|
|
1342
1420
|
config[k] = v;
|
|
1343
1421
|
}
|
|
1344
1422
|
}
|
|
1345
|
-
const instance = new DynClass(config);
|
|
1423
|
+
const instance = new DynClass(config) as ExternalClassInstance;
|
|
1346
1424
|
|
|
1347
|
-
let value;
|
|
1425
|
+
let value: unknown;
|
|
1348
1426
|
if (typeof instance.resolve === "function") {
|
|
1349
1427
|
value = await instance.resolve();
|
|
1350
1428
|
} else if ("value" in instance) {
|
|
@@ -1395,7 +1473,12 @@ function classFromSchema(classDef: JxClassDef) {
|
|
|
1395
1473
|
}
|
|
1396
1474
|
if (ctor?.body) {
|
|
1397
1475
|
const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
|
|
1398
|
-
|
|
1476
|
+
(
|
|
1477
|
+
new Function("config", bodyStr) as (
|
|
1478
|
+
this: unknown,
|
|
1479
|
+
config: Record<string, unknown>,
|
|
1480
|
+
) => void
|
|
1481
|
+
).call(this, config);
|
|
1399
1482
|
}
|
|
1400
1483
|
}
|
|
1401
1484
|
}
|
|
@@ -1528,17 +1611,17 @@ async function resolveServerFunction(
|
|
|
1528
1611
|
const src = def.$src;
|
|
1529
1612
|
const exportName = def.$export;
|
|
1530
1613
|
|
|
1531
|
-
let mod;
|
|
1614
|
+
let mod: ImportedModule;
|
|
1532
1615
|
if (_moduleCache.has(src)) {
|
|
1533
|
-
mod = _moduleCache.get(src)
|
|
1616
|
+
mod = _moduleCache.get(src)!;
|
|
1534
1617
|
} else {
|
|
1535
1618
|
try {
|
|
1536
|
-
mod = await import(src);
|
|
1619
|
+
mod = (await import(src)) as ImportedModule;
|
|
1537
1620
|
} catch {
|
|
1538
1621
|
if (base) {
|
|
1539
1622
|
try {
|
|
1540
1623
|
const resolvedSrc = new URL(src, base).href;
|
|
1541
|
-
mod = await import(resolvedSrc);
|
|
1624
|
+
mod = (await import(resolvedSrc)) as ImportedModule;
|
|
1542
1625
|
} catch {
|
|
1543
1626
|
// Module cannot run in the browser — fall back to dev server proxy
|
|
1544
1627
|
return resolveServerFunctionViaProxy(def, state, key, base);
|
|
@@ -1550,13 +1633,14 @@ async function resolveServerFunction(
|
|
|
1550
1633
|
_moduleCache.set(src, mod);
|
|
1551
1634
|
}
|
|
1552
1635
|
|
|
1553
|
-
const
|
|
1554
|
-
if (!
|
|
1636
|
+
const candidate = mod[exportName] ?? mod.default?.[exportName];
|
|
1637
|
+
if (!candidate) {
|
|
1555
1638
|
throw new Error(`Jx: export "${exportName}" not found in "${src}" for "${key}"`);
|
|
1556
1639
|
}
|
|
1557
|
-
if (typeof
|
|
1640
|
+
if (typeof candidate !== "function") {
|
|
1558
1641
|
throw new TypeError(`Jx: "${exportName}" from "${src}" is not a function`);
|
|
1559
1642
|
}
|
|
1643
|
+
const fn = candidate as (args: JxScope) => Promise<unknown>;
|
|
1560
1644
|
|
|
1561
1645
|
const rawArgs = def.arguments ?? {};
|
|
1562
1646
|
const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
|
|
@@ -1672,7 +1756,7 @@ export function resolveRef(refPath: string, state: JxScope) {
|
|
|
1672
1756
|
const parts = refPath.split("/");
|
|
1673
1757
|
const [, key] = parts; // "item" or "index"
|
|
1674
1758
|
const map = state.$map as Record<string, unknown> | undefined;
|
|
1675
|
-
const base = map?.[key] ?? state[`$map/${key}`];
|
|
1759
|
+
const base = map?.[key!] ?? state[`$map/${key}`];
|
|
1676
1760
|
return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
|
|
1677
1761
|
}
|
|
1678
1762
|
if (refPath.startsWith("#/state/")) {
|
|
@@ -1742,7 +1826,7 @@ function getPath(obj: unknown, path: string) {
|
|
|
1742
1826
|
* @returns {JxScope}
|
|
1743
1827
|
*/
|
|
1744
1828
|
function mergeProps(def: JxElement, parentState: JxScope): JxScope {
|
|
1745
|
-
const child = Object.create(parentState);
|
|
1829
|
+
const child = Object.create(parentState) as JxScope;
|
|
1746
1830
|
for (const [k, v] of Object.entries(def.$props ?? {})) {
|
|
1747
1831
|
child[k] = isRefObj(v) ? resolveRef(v.$ref, parentState) : v;
|
|
1748
1832
|
}
|
|
@@ -1927,7 +2011,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
1927
2011
|
const propsAttr = this.dataset.jxProps;
|
|
1928
2012
|
if (propsAttr) {
|
|
1929
2013
|
try {
|
|
1930
|
-
const props = JSON.parse(propsAttr)
|
|
2014
|
+
const props = JSON.parse(propsAttr) as Record<string, unknown>;
|
|
1931
2015
|
for (const [key, val] of Object.entries(props)) {
|
|
1932
2016
|
if (key in (def.state ?? {})) {
|
|
1933
2017
|
state[key] = val;
|
|
@@ -1984,19 +2068,19 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
1984
2068
|
// Lifecycle: onMount
|
|
1985
2069
|
const { onMount } = state;
|
|
1986
2070
|
if (typeof onMount === "function") {
|
|
1987
|
-
queueMicrotask(() => onMount(state));
|
|
2071
|
+
queueMicrotask(() => (onMount as (s: JxScope) => unknown)(state));
|
|
1988
2072
|
}
|
|
1989
2073
|
}
|
|
1990
2074
|
|
|
1991
2075
|
disconnectedCallback() {
|
|
1992
2076
|
if (typeof this._state?.onUnmount === "function") {
|
|
1993
|
-
this._state.onUnmount(this._state);
|
|
2077
|
+
(this._state.onUnmount as (s: JxScope) => unknown)(this._state);
|
|
1994
2078
|
}
|
|
1995
2079
|
}
|
|
1996
2080
|
|
|
1997
2081
|
adoptedCallback() {
|
|
1998
2082
|
if (typeof this._state?.onAdopted === "function") {
|
|
1999
|
-
this._state.onAdopted(this._state);
|
|
2083
|
+
(this._state.onAdopted as (s: JxScope) => unknown)(this._state);
|
|
2000
2084
|
}
|
|
2001
2085
|
}
|
|
2002
2086
|
|
|
@@ -2037,7 +2121,7 @@ function renderCustomElementWithProps(
|
|
|
2037
2121
|
const el = document.createElement(def.tagName ?? "div");
|
|
2038
2122
|
|
|
2039
2123
|
if (options?.onNodeCreated) {
|
|
2040
|
-
options.onNodeCreated(el, path ?? [], def);
|
|
2124
|
+
options.onNodeCreated(el, path ?? [], def, state);
|
|
2041
2125
|
}
|
|
2042
2126
|
|
|
2043
2127
|
// Set JS properties from $props (before connection)
|
|
@@ -2067,7 +2151,7 @@ function renderCustomElementWithProps(
|
|
|
2067
2151
|
const children = Array.isArray(def.children) ? def.children : [];
|
|
2068
2152
|
for (let i = 0; i < children.length; i++) {
|
|
2069
2153
|
const childOpts = options && path ? { ...options, _path: [...path, "children", i] } : undefined;
|
|
2070
|
-
el.append(renderNode(children[i]
|
|
2154
|
+
el.append(renderNode(children[i]!, state, childOpts));
|
|
2071
2155
|
}
|
|
2072
2156
|
|
|
2073
2157
|
return el;
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
import type { JxElement, JxPath } from "@jxsuite/schema/types";
|
|
3
3
|
|
|
4
|
-
export type { JxPath };
|
|
4
|
+
export type { JxPath } from "@jxsuite/schema/types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The live reactive scope: a prototype-chained object whose keys are user-defined state names.
|
|
@@ -15,7 +15,16 @@ export type JxEventHandler = (scope: JxScope, event: Event) => unknown;
|
|
|
15
15
|
|
|
16
16
|
export interface JxRenderOptions {
|
|
17
17
|
_path?: JxPath;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Called for each created node. `state` is the local scope the node's children render with —
|
|
20
|
+
* callers can capture it to re-render a subtree in isolation later.
|
|
21
|
+
*/
|
|
22
|
+
onNodeCreated?: (
|
|
23
|
+
el: HTMLElement | Text,
|
|
24
|
+
path: JxPath,
|
|
25
|
+
def: JxElement | string,
|
|
26
|
+
state?: JxScope,
|
|
27
|
+
) => void;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
export interface DynamicClass {
|