@jxsuite/runtime 4.0.0 → 4.0.2
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 +99 -17
- package/dist/runtime.js.map +5 -5
- package/package.json +3 -3
- package/src/runtime.ts +309 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"upgrade": "bun update --latest && bun install"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jxsuite/schema": "^2.
|
|
37
|
-
"@vue/reactivity": "3.5.
|
|
36
|
+
"@jxsuite/schema": "^2.3.0",
|
|
37
|
+
"@vue/reactivity": "3.5.43"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@happy-dom/global-registrator": "^20.14.5"
|
package/src/runtime.ts
CHANGED
|
@@ -1915,27 +1915,30 @@ const SCOPE_TOKEN = "jx-scope";
|
|
|
1915
1915
|
* wholesale. Sweeping on `isConnected` instead would be wrong rather than merely redundant: an
|
|
1916
1916
|
* element is routinely styled before it is inserted.
|
|
1917
1917
|
*
|
|
1918
|
+
* Both call sites reach this only once an element is already IN `scratchOwners`, and the only place
|
|
1919
|
+
* that happens (`scratchOwners.set`, below) always opens `scratch` first — so it is never still
|
|
1920
|
+
* null here.
|
|
1921
|
+
*
|
|
1918
1922
|
* @param {SheetState} state
|
|
1919
1923
|
*/
|
|
1920
1924
|
function rewriteScratch(state: SheetState) {
|
|
1921
|
-
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
state.scratch.replaceAll([...state.scratchOwners.values()].flat());
|
|
1925
|
+
state.scratch!.replaceAll([...state.scratchOwners.values()].flat());
|
|
1925
1926
|
}
|
|
1926
1927
|
|
|
1927
1928
|
/**
|
|
1928
1929
|
* Drop one reference to an interned rule set, removing its rules when the last one goes.
|
|
1929
1930
|
*
|
|
1931
|
+
* `key` always names an entry `retainInterned` already created: {@link releaseElementStyles}'s two
|
|
1932
|
+
* call sites pass back exactly the `entry.interned`/`entry.hoisted` keys it was handed at
|
|
1933
|
+
* `retainInterned` time, and a stale `state` (one `resetDocumentStyles` already replaced) is turned
|
|
1934
|
+
* away before either call site is reached.
|
|
1935
|
+
*
|
|
1930
1936
|
* @param {SheetState} state
|
|
1931
1937
|
* @param {Map<string, InternEntry>} table
|
|
1932
1938
|
* @param {string} key
|
|
1933
1939
|
*/
|
|
1934
1940
|
function releaseInterned(state: SheetState, table: Map<string, InternEntry>, key: string) {
|
|
1935
|
-
const entry = table.get(key)
|
|
1936
|
-
if (!entry) {
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1941
|
+
const entry = table.get(key)!;
|
|
1939
1942
|
entry.refs -= 1;
|
|
1940
1943
|
if (entry.refs <= 0) {
|
|
1941
1944
|
table.delete(key);
|
|
@@ -2467,6 +2470,11 @@ function renderMappedArrayInto(
|
|
|
2467
2470
|
parentEl.append(anchor);
|
|
2468
2471
|
const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef, key: keyRef } = arrayDef;
|
|
2469
2472
|
const keyPointer = isRefObj(keyRef) ? keyRef.$ref : null;
|
|
2473
|
+
/* The instantiation chain the list belongs to (spec.md §16.9), for the same reason
|
|
2474
|
+
{@link renderSwitch} keeps one: a row that arrives with new data is inserted from an effect
|
|
2475
|
+
re-run, and the instance it holds must connect under the chain of the definition that drew
|
|
2476
|
+
the list. */
|
|
2477
|
+
const instancePath = _instancePath;
|
|
2470
2478
|
|
|
2471
2479
|
let warned = false;
|
|
2472
2480
|
const warnOnce = (message: string): void => {
|
|
@@ -2572,7 +2580,7 @@ function renderMappedArrayInto(
|
|
|
2572
2580
|
|
|
2573
2581
|
pauseTracking();
|
|
2574
2582
|
try {
|
|
2575
|
-
reconcile(list, keys);
|
|
2583
|
+
underInstancePath(reexpand(instancePath), () => reconcile(list, keys));
|
|
2576
2584
|
} finally {
|
|
2577
2585
|
resetTracking();
|
|
2578
2586
|
}
|
|
@@ -2687,6 +2695,11 @@ export function resolveTagName(tagName: unknown, scope: JxScope): string {
|
|
|
2687
2695
|
function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions) {
|
|
2688
2696
|
const path = options?._path ?? [];
|
|
2689
2697
|
const container = document.createElement(resolveTagName(def.tagName, state));
|
|
2698
|
+
/* The instantiation chain this switch belongs to (spec.md §16.9), read now because the effect
|
|
2699
|
+
below re-runs with nothing on the stack. A case that flips after the first paint puts it back
|
|
2700
|
+
around the insertion, so an instance the new case creates connects under the chain of the
|
|
2701
|
+
definition that drew the switch rather than under none. See {@link _instancePath}. */
|
|
2702
|
+
const instancePath = _instancePath;
|
|
2690
2703
|
|
|
2691
2704
|
if (options?.onNodeCreated) {
|
|
2692
2705
|
options.onNodeCreated(container, path, def, state);
|
|
@@ -2753,7 +2766,9 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
|
|
|
2753
2766
|
return;
|
|
2754
2767
|
}
|
|
2755
2768
|
container.replaceChildren();
|
|
2756
|
-
|
|
2769
|
+
underInstancePath(reexpand(instancePath), () =>
|
|
2770
|
+
scope.run(() => container.append(renderNode(doc, childScope, childOpts))),
|
|
2771
|
+
);
|
|
2757
2772
|
})
|
|
2758
2773
|
.catch((error: unknown) =>
|
|
2759
2774
|
console.error("Jx $switch: failed to load external case", caseDef.$ref, error),
|
|
@@ -2766,7 +2781,9 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
|
|
|
2766
2781
|
// Change to anything it read. The bindings inside it track for themselves.
|
|
2767
2782
|
pauseTracking();
|
|
2768
2783
|
try {
|
|
2769
|
-
|
|
2784
|
+
underInstancePath(reexpand(instancePath), () =>
|
|
2785
|
+
scope.run(() => container.append(renderNode(caseDef, state, childOpts))),
|
|
2786
|
+
);
|
|
2770
2787
|
} finally {
|
|
2771
2788
|
resetTracking();
|
|
2772
2789
|
}
|
|
@@ -3117,6 +3134,9 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
|
|
|
3117
3134
|
* Resolve a .class.json schema-defined class. Fetches the schema, follows $implementation if
|
|
3118
3135
|
* hybrid, or constructs dynamically if self-contained.
|
|
3119
3136
|
*
|
|
3137
|
+
* Its one caller, {@link resolveExternalPrototype}, already rejects a missing `$src` before this
|
|
3138
|
+
* runs, so `def.$src` is never empty here.
|
|
3139
|
+
*
|
|
3120
3140
|
* @param {JxPrototypeDef} def
|
|
3121
3141
|
* @param {JxScope} state
|
|
3122
3142
|
* @param {string} key
|
|
@@ -3124,10 +3144,7 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
|
|
|
3124
3144
|
* @returns {Promise<unknown>}
|
|
3125
3145
|
*/
|
|
3126
3146
|
async function resolveClassJson(def: JxPrototypeDef, state: JxScope, key: string, base?: string) {
|
|
3127
|
-
const src = def.$src
|
|
3128
|
-
if (!src) {
|
|
3129
|
-
throw new Error(`Jx: class entry '${key}' has no $src`);
|
|
3130
|
-
}
|
|
3147
|
+
const src = def.$src!;
|
|
3131
3148
|
let classDef: JxClassDef;
|
|
3132
3149
|
|
|
3133
3150
|
// Bare specifiers (package references like @scope/pkg/file) can't be fetched directly —
|
|
@@ -3943,6 +3960,219 @@ function injectHead(entries: JxHeadEntry[], _base: string) {
|
|
|
3943
3960
|
}
|
|
3944
3961
|
}
|
|
3945
3962
|
|
|
3963
|
+
// ─── Instantiation chain (spec.md §16.9) ─────────────────────────────────────
|
|
3964
|
+
|
|
3965
|
+
/**
|
|
3966
|
+
* How deep component nesting may go before an instance refuses to render.
|
|
3967
|
+
*
|
|
3968
|
+
* The same number the compiler holds in `MAX_COMPONENT_NESTING` (`@jxsuite/compiler`, `shared.ts`),
|
|
3969
|
+
* and for the same reason: the cycle check in {@link extendInstancePath} catches a definition that
|
|
3970
|
+
* renders ITSELF — same tag, same props — which is the only shape that can never terminate, while a
|
|
3971
|
+
* self-reference whose props change at every level is data-driven recursion that a `$switch` may or
|
|
3972
|
+
* may not bottom out. The seen-set cannot tell those apart, so this cap is what turns a forgotten
|
|
3973
|
+
* stop into a diagnostic instead of a page that never settles. It is written twice because the
|
|
3974
|
+
* compiler depends on the runtime and not the other way round, and the runtime's browser bundle
|
|
3975
|
+
* cannot reach into the build tool; spec.md §16.9 is the one statement both are held to, and the
|
|
3976
|
+
* compiler's `nesting-cap-agreement.test.ts` asserts its copy against this export, so the two
|
|
3977
|
+
* numbers cannot part without a red test in the workspace that owns the copy.
|
|
3978
|
+
*/
|
|
3979
|
+
export const MAX_COMPONENT_NESTING = 32;
|
|
3980
|
+
|
|
3981
|
+
/** One instance on the chain: its tag, and the props it was given, as an identity. */
|
|
3982
|
+
interface InstanceFrame {
|
|
3983
|
+
tag: string;
|
|
3984
|
+
/** Every value the instance supplied that its definition's state read (§16.9). */
|
|
3985
|
+
props: Record<string, unknown>;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
/**
|
|
3989
|
+
* The chain an instance is created under, and where on it the current expansion began.
|
|
3990
|
+
*
|
|
3991
|
+
* `frames` is every instance from the page-level one down, outermost first. `expansion` is the
|
|
3992
|
+
* index of the first frame that belongs to the SAME synchronous pass as the instance being created:
|
|
3993
|
+
* the cycle check (§16.9 rule 1) looks back no further than that, while the cap (rule 2) counts
|
|
3994
|
+
* every frame. The distinction is what an effect re-run does to certainty. Along one pass a
|
|
3995
|
+
* repeated frame is a render that repeats itself, deterministically, forever. Across a re-run it is
|
|
3996
|
+
* not: the frames above the boundary rendered to completion once, and whether the page asks for one
|
|
3997
|
+
* of them again is its data — an `onMount` that flips a `$switch` into the same tag every time, or
|
|
3998
|
+
* a click that opens one more level of a node that starts closed — which the runtime cannot tell
|
|
3999
|
+
* apart. So the cycle check is not asked across the boundary, and the loop that lives in re-runs is
|
|
4000
|
+
* ended by the cap instead: thirty-three empty hosts and a diagnostic, not a page that never
|
|
4001
|
+
* settles.
|
|
4002
|
+
*/
|
|
4003
|
+
interface InstancePath {
|
|
4004
|
+
frames: readonly InstanceFrame[];
|
|
4005
|
+
expansion: number;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
/** The chain a page-level render runs under: nothing above it. */
|
|
4009
|
+
const ROOT_INSTANCE_PATH: InstancePath = { expansion: 0, frames: [] };
|
|
4010
|
+
|
|
4011
|
+
/**
|
|
4012
|
+
* The chain of instances whose definitions are being rendered RIGHT NOW.
|
|
4013
|
+
*
|
|
4014
|
+
* A module-level stack rather than a WeakMap from element to chain, and it is worth saying why that
|
|
4015
|
+
* survives `connectedCallback` being async. The callback awaits `buildScope` before it touches the
|
|
4016
|
+
* DOM, and by the time it resumes, whatever the enclosing instance pushed here has long been popped
|
|
4017
|
+
* — so the stack alone would be empty for every instance and catch nothing. What saves it is WHEN
|
|
4018
|
+
* an instance connects: `this.append(child)` in the enclosing render runs the child's
|
|
4019
|
+
* `connectedCallback` synchronously, and every way a definition brings an instance into being —
|
|
4020
|
+
* `renderCustomElementWithProps`, the bare-tag branch of `renderNode`, a nested element inside
|
|
4021
|
+
* either, and a prerendered host drawing its children again as it upgrades — appends it inside that
|
|
4022
|
+
* same synchronous stretch. So the callback reads the stack ONCE, before its first `await`, and
|
|
4023
|
+
* carries the copy across the gap. A WeakMap would need a write at each of those creation sites to
|
|
4024
|
+
* say the same thing.
|
|
4025
|
+
*
|
|
4026
|
+
* A subtree an effect renders LATER — a `$switch` case that flips after the first paint, a `$map`
|
|
4027
|
+
* row that arrives with new data — connects with nothing on the stack, because no enclosing render
|
|
4028
|
+
* is running when the effect re-runs; a write from `onMount` or a click is what triggered it. So
|
|
4029
|
+
* the two renderers that create nodes from an effect each capture the stack when they are first
|
|
4030
|
+
* rendered (which is inside the enclosing pass) and put it back, re-expanded, around every later
|
|
4031
|
+
* insertion ({@link underInstancePath}). That is the one thing this stack needs beyond the
|
|
4032
|
+
* synchronous stretch, and it is what makes the `onMount`-flip shape a diagnostic rather than a
|
|
4033
|
+
* hang. Reading `@vue/reactivity`'s `getCurrentScope()` there instead would find nothing: an effect
|
|
4034
|
+
* re-run restores no scope, and a case or a row renders in a DETACHED scope with no parent.
|
|
4035
|
+
*/
|
|
4036
|
+
let _instancePath: InstancePath = ROOT_INSTANCE_PATH;
|
|
4037
|
+
|
|
4038
|
+
/**
|
|
4039
|
+
* Run `fn` with `path` as the chain, so every instance that CONNECTS during it reads that chain.
|
|
4040
|
+
*
|
|
4041
|
+
* Restored rather than cleared afterwards: the enclosing render's chain is what was there, and a
|
|
4042
|
+
* restore needs no argument about whether anything was.
|
|
4043
|
+
*
|
|
4044
|
+
* @param {InstancePath} path - The chain the instances `fn` connects are created under
|
|
4045
|
+
* @param {() => T} fn - The render, or the insertion that connects what a render made
|
|
4046
|
+
* @returns {T} What `fn` returned
|
|
4047
|
+
*/
|
|
4048
|
+
function underInstancePath<T>(path: InstancePath, fn: () => T): T {
|
|
4049
|
+
const was = _instancePath;
|
|
4050
|
+
_instancePath = path;
|
|
4051
|
+
try {
|
|
4052
|
+
return fn();
|
|
4053
|
+
} finally {
|
|
4054
|
+
_instancePath = was;
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
/**
|
|
4059
|
+
* The chain a later insertion runs under: the same frames, with the expansion beginning now.
|
|
4060
|
+
*
|
|
4061
|
+
* Called on EVERY run of a `$switch` case or a `$map` reconcile, the first one included — harmless
|
|
4062
|
+
* there, because a first run builds into a detached container and nothing connects until the
|
|
4063
|
+
* enclosing pass appends it, under the enclosing pass's own chain.
|
|
4064
|
+
*
|
|
4065
|
+
* @param {InstancePath} path - The chain captured when the renderer was first rendered
|
|
4066
|
+
* @returns {InstancePath}
|
|
4067
|
+
*/
|
|
4068
|
+
function reexpand(path: InstancePath): InstancePath {
|
|
4069
|
+
return { expansion: path.frames.length, frames: path.frames };
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
/**
|
|
4073
|
+
* Whether two prop values are the same input to a definition.
|
|
4074
|
+
*
|
|
4075
|
+
* Reference equality first — a `$ref` into the parent's state forwards the same primitive, the same
|
|
4076
|
+
* reactive proxy or the same function to the next level — and structural equality for plain data
|
|
4077
|
+
* rebuilt at each level (`data-jx-props` parsed twice is two objects with one meaning). Plain data
|
|
4078
|
+
* is what JSON can describe: an array, or an object with no prototype but `Object`'s, reactive or
|
|
4079
|
+
* raw. Anything else — a `Map`, a `Set`, a class instance, a circular object — is different from
|
|
4080
|
+
* everything but itself, because its JSON form would not say what it holds and two different values
|
|
4081
|
+
* that serialise alike would close a cycle that was never there. The cost of that policy is a loop
|
|
4082
|
+
* the depth cap catches instead; the alternative is a false diagnostic on a page that would have
|
|
4083
|
+
* ended. Inside plain data the comparison IS the JSON form, so a member JSON cannot see (a
|
|
4084
|
+
* function, a `Map` two levels down) is not seen here either; §16.9 says so.
|
|
4085
|
+
*
|
|
4086
|
+
* @param {unknown} a
|
|
4087
|
+
* @param {unknown} b
|
|
4088
|
+
* @returns {boolean}
|
|
4089
|
+
*/
|
|
4090
|
+
function samePropValue(a: unknown, b: unknown): boolean {
|
|
4091
|
+
if (Object.is(a, b)) {
|
|
4092
|
+
return true;
|
|
4093
|
+
}
|
|
4094
|
+
if (!isPlainData(a) || !isPlainData(b)) {
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
try {
|
|
4098
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
4099
|
+
} catch {
|
|
4100
|
+
return false;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
/**
|
|
4105
|
+
* Whether a value is an array or a plain object — the shapes whose JSON form is the whole value.
|
|
4106
|
+
*
|
|
4107
|
+
* @param {unknown} value
|
|
4108
|
+
* @returns {value is object}
|
|
4109
|
+
*/
|
|
4110
|
+
function isPlainData(value: unknown): value is object {
|
|
4111
|
+
if (typeof value !== "object" || value === null) {
|
|
4112
|
+
return false;
|
|
4113
|
+
}
|
|
4114
|
+
const raw: unknown = toRaw(value);
|
|
4115
|
+
if (Array.isArray(raw)) {
|
|
4116
|
+
return true;
|
|
4117
|
+
}
|
|
4118
|
+
const proto: unknown = Object.getPrototypeOf(raw);
|
|
4119
|
+
return proto === Object.prototype || proto === null;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
/**
|
|
4123
|
+
* Whether two frames are the same instance: one tag, one set of prop names, equal values.
|
|
4124
|
+
*
|
|
4125
|
+
* @param {InstanceFrame} a
|
|
4126
|
+
* @param {InstanceFrame} b
|
|
4127
|
+
* @returns {boolean}
|
|
4128
|
+
*/
|
|
4129
|
+
function sameFrame(a: InstanceFrame, b: InstanceFrame): boolean {
|
|
4130
|
+
if (a.tag !== b.tag) {
|
|
4131
|
+
return false;
|
|
4132
|
+
}
|
|
4133
|
+
const keys = Object.keys(a.props);
|
|
4134
|
+
if (keys.length !== Object.keys(b.props).length) {
|
|
4135
|
+
return false;
|
|
4136
|
+
}
|
|
4137
|
+
return keys.every(
|
|
4138
|
+
(key) => Object.hasOwn(b.props, key) && samePropValue(a.props[key], b.props[key]),
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
/**
|
|
4143
|
+
* The chain an instance's own render runs under: the enclosing chain plus itself — or the
|
|
4144
|
+
* diagnostic that says why it must not render at all.
|
|
4145
|
+
*
|
|
4146
|
+
* The two messages are the compiler's, word for word (`renderComponentInstance` in
|
|
4147
|
+
* `@jxsuite/compiler`), so a page that fails to build and a canvas that fails to paint say the same
|
|
4148
|
+
* thing about the same definition. Meeting a frame already on the chain, within the current
|
|
4149
|
+
* expansion, is the instance rendering itself with the same props, which never ends; a chain at the
|
|
4150
|
+
* cap is a self-reference whose props change — or whose repeats live in re-runs, where the cycle
|
|
4151
|
+
* check is not asked ({@link InstancePath}) — and whose stop case never came.
|
|
4152
|
+
*
|
|
4153
|
+
* @param {InstancePath} path - The chain the instance was created under
|
|
4154
|
+
* @param {InstanceFrame} frame - The instance about to render
|
|
4155
|
+
* @returns {InstancePath} The chain its children render under
|
|
4156
|
+
* @throws {Error} The named diagnostic — never a stack overflow, never a hang
|
|
4157
|
+
* @docs framework/concepts/components
|
|
4158
|
+
*/
|
|
4159
|
+
function extendInstancePath(path: InstancePath, frame: InstanceFrame): InstancePath {
|
|
4160
|
+
const chain = [...path.frames.map((f) => f.tag), frame.tag].join(" → ");
|
|
4161
|
+
if (path.frames.slice(path.expansion).some((f) => sameFrame(f, frame))) {
|
|
4162
|
+
throw new Error(
|
|
4163
|
+
`Component <${frame.tag}> renders itself: ${chain}. A component cannot appear inside its own ` +
|
|
4164
|
+
`definition with the same props — the expansion would never end.`,
|
|
4165
|
+
);
|
|
4166
|
+
}
|
|
4167
|
+
if (path.frames.length >= MAX_COMPONENT_NESTING) {
|
|
4168
|
+
throw new Error(
|
|
4169
|
+
`Component nesting exceeds ${MAX_COMPONENT_NESTING} levels: ${chain}. A component that ` +
|
|
4170
|
+
`renders itself with changing props needs a case that stops.`,
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
return { expansion: path.expansion, frames: [...path.frames, frame] };
|
|
4174
|
+
}
|
|
4175
|
+
|
|
3946
4176
|
/**
|
|
3947
4177
|
* Register a custom element from a Jx document.
|
|
3948
4178
|
*
|
|
@@ -4009,6 +4239,17 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4009
4239
|
}
|
|
4010
4240
|
this._jxInitialized = true;
|
|
4011
4241
|
|
|
4242
|
+
/* The chain this instance was created under, read BEFORE the first `await`: the enclosing
|
|
4243
|
+
render is still on the stack now and will not be when this callback resumes. See
|
|
4244
|
+
{@link _instancePath} for why that ordering is the whole mechanism. */
|
|
4245
|
+
const enclosingPath = _instancePath;
|
|
4246
|
+
/* What this instance was given, recorded beside each write into state below. It is the
|
|
4247
|
+
instance's identity on the chain (§16.9): two nested instances of one tag that were given
|
|
4248
|
+
the same values render the same thing, forever. Only a value the definition's state READS
|
|
4249
|
+
is recorded — a `$props` key no state entry declares changes nothing about the render, so
|
|
4250
|
+
a definition that "varies" one at each level is still rendering itself. */
|
|
4251
|
+
const supplied: Record<string, unknown> = {};
|
|
4252
|
+
|
|
4012
4253
|
// The definition as it stands NOW — a redefinition since this class was made is honoured.
|
|
4013
4254
|
const live = _elementDefs.get(tagName) ?? { base: initialBase, doc: initialDef };
|
|
4014
4255
|
const def = live.doc;
|
|
@@ -4026,6 +4267,9 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4026
4267
|
for (const attr of observedAttrs) {
|
|
4027
4268
|
if (this.hasAttribute(attr)) {
|
|
4028
4269
|
absorbAttribute(state, attr, this.getAttribute(attr));
|
|
4270
|
+
// The raw string, not the coerced value: coercion is a function of it, so two equal
|
|
4271
|
+
// Strings are one input.
|
|
4272
|
+
supplied[attr] = this.getAttribute(attr);
|
|
4029
4273
|
}
|
|
4030
4274
|
}
|
|
4031
4275
|
|
|
@@ -4039,6 +4283,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4039
4283
|
}
|
|
4040
4284
|
if (key in (def.state ?? {})) {
|
|
4041
4285
|
state[key] = val;
|
|
4286
|
+
supplied[key] = val;
|
|
4042
4287
|
}
|
|
4043
4288
|
}
|
|
4044
4289
|
} catch {}
|
|
@@ -4060,6 +4305,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4060
4305
|
}
|
|
4061
4306
|
if (key in (def.state ?? {})) {
|
|
4062
4307
|
state[key] = this.getAttribute(name);
|
|
4308
|
+
supplied[key] = this.getAttribute(name);
|
|
4063
4309
|
this.removeAttribute(name);
|
|
4064
4310
|
}
|
|
4065
4311
|
}
|
|
@@ -4075,8 +4321,49 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4075
4321
|
instanceSupplies(this, key)
|
|
4076
4322
|
) {
|
|
4077
4323
|
state[key] = (this as Record<string, unknown>)[key];
|
|
4324
|
+
supplied[key] = (this as Record<string, unknown>)[key];
|
|
4078
4325
|
}
|
|
4079
4326
|
}
|
|
4327
|
+
|
|
4328
|
+
/* Every input is known now, so this is where an instance that would never finish is
|
|
4329
|
+
refused (§16.9): before it has a state, a style or a child, so a refused host is an empty
|
|
4330
|
+
element the page renders around — emptied of what the definition wrote into it for its
|
|
4331
|
+
slots too, since nothing will distribute that. The diagnostic is REPORTED rather than
|
|
4332
|
+
thrown onward: a throw from an async `connectedCallback` is an unhandled rejection that
|
|
4333
|
+
names no element and stops nothing. It goes two ways. The console is the runtime's
|
|
4334
|
+
surface for a render that fails after the page has settled (`$switch` external cases and
|
|
4335
|
+
the dev proxies report there too). The `jx-error` event is the surface a HOST can hold:
|
|
4336
|
+
it bubbles from the refused element with the error on it, so a page, or the studio canvas
|
|
4337
|
+
wrapping one, can collect it where a console line inside an iframe reaches nobody. The
|
|
4338
|
+
rest of the page keeps rendering, which is the point.
|
|
4339
|
+
|
|
4340
|
+
Neither surface is used for a host that is no longer in the document. A refused instance
|
|
4341
|
+
was connected when this callback began, and one that is not by the time the verdict is in
|
|
4342
|
+
belongs to a subtree the page discarded during the `await` above: a prerendered host of the
|
|
4343
|
+
same tag that was parsed INSIDE another and re-rendered from the definition by the outer
|
|
4344
|
+
one's upgrade, which captures it as slot content and empties itself. Its replacement is
|
|
4345
|
+
the instance that reports, on the chain the outer upgrade draws; reporting from the
|
|
4346
|
+
discarded copy too said one defect twice. The refusal itself still stands, so a host
|
|
4347
|
+
re-attached later is an empty element and not a hang. */
|
|
4348
|
+
let ownPath: InstancePath;
|
|
4349
|
+
try {
|
|
4350
|
+
ownPath = extendInstancePath(enclosingPath, { props: supplied, tag: tagName });
|
|
4351
|
+
} catch (error) {
|
|
4352
|
+
this.replaceChildren();
|
|
4353
|
+
if (!this.isConnected) {
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
console.error(`Jx: <${tagName}> was not rendered:`, error);
|
|
4357
|
+
this.dispatchEvent(
|
|
4358
|
+
new ErrorEvent("jx-error", {
|
|
4359
|
+
bubbles: true,
|
|
4360
|
+
composed: true,
|
|
4361
|
+
error,
|
|
4362
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4363
|
+
}),
|
|
4364
|
+
);
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4080
4367
|
/*
|
|
4081
4368
|
* Set up property getters/setters that forward into reactive state. Private entries get NO
|
|
4082
4369
|
* accessor: the property-first interface IS the props mechanism, so defining one would leave
|
|
@@ -4134,10 +4421,14 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
4134
4421
|
themselves. Restored rather than cleared, because one definition may render another. */
|
|
4135
4422
|
const wasInside = _canvasInsideStampedHost;
|
|
4136
4423
|
_canvasInsideStampedHost ||= this.dataset.jxPath !== undefined;
|
|
4424
|
+
/* And the chain, for the same span: an instance this definition draws connects inside
|
|
4425
|
+
`this.append` below and reads it there ({@link underInstancePath}). */
|
|
4137
4426
|
try {
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4427
|
+
underInstancePath(ownPath, () => {
|
|
4428
|
+
for (const childDef of children) {
|
|
4429
|
+
this.append(renderNode(childDef, state));
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4141
4432
|
} finally {
|
|
4142
4433
|
_canvasInsideStampedHost = wasInside;
|
|
4143
4434
|
}
|