@jay-framework/stack-client-runtime 0.12.0 → 0.14.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/index.cjs +193 -37
- package/dist/index.d.ts +29 -5
- package/dist/index.js +194 -38
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -80,23 +80,56 @@ function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMa
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
const HEADLESS_INSTANCES = runtime.createJayContext();
|
|
83
|
-
function makeSignals$
|
|
83
|
+
function makeSignals$2(obj) {
|
|
84
84
|
return Object.keys(obj).reduce((signals, key) => {
|
|
85
85
|
signals[key] = component.createSignal(obj[key]);
|
|
86
86
|
return signals;
|
|
87
87
|
}, {});
|
|
88
88
|
}
|
|
89
|
-
function makeHeadlessInstanceComponent(preRender,
|
|
90
|
-
const
|
|
89
|
+
function makeHeadlessInstanceComponent(preRender, componentDef, coordinateKey) {
|
|
90
|
+
const interactiveConstructor = componentDef.comp;
|
|
91
|
+
const resolvedContexts = componentDef.contexts ?? [];
|
|
92
|
+
const clientDefaults = componentDef.clientDefaults;
|
|
93
|
+
const wrappedConstructor = (signalProps, refs, ...pluginResolvedContexts) => {
|
|
91
94
|
const instanceData = runtime.useContext(HEADLESS_INSTANCES);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
95
|
+
const resolvedKey = typeof coordinateKey === "function" ? coordinateKey(runtime.currentConstructionContext()?.dataIds ?? []) : coordinateKey;
|
|
96
|
+
const suffixKey = resolvedKey.includes("/") && resolvedKey.includes(":") ? resolvedKey.split("/").find((s) => s.includes(":")) ?? resolvedKey : resolvedKey;
|
|
97
|
+
const fastVS = instanceData?.viewStates?.[resolvedKey] ?? instanceData?.viewStates?.[suffixKey];
|
|
98
|
+
const cf = instanceData?.carryForwards?.[resolvedKey] ?? instanceData?.carryForwards?.[suffixKey];
|
|
99
|
+
let resolvedFastVS;
|
|
100
|
+
let resolvedCf;
|
|
101
|
+
if (fastVS) {
|
|
102
|
+
resolvedFastVS = fastVS;
|
|
103
|
+
resolvedCf = cf || {};
|
|
104
|
+
} else if (clientDefaults) {
|
|
105
|
+
const rawProps = signalProps.props();
|
|
106
|
+
const defaults = clientDefaults(rawProps);
|
|
107
|
+
resolvedFastVS = defaults.viewState;
|
|
108
|
+
resolvedCf = defaults.carryForward ?? {};
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(
|
|
111
|
+
`[Jay] Headless instance "${resolvedKey}" has no server data and no clientDefaults. Add .withClientDefaults() to the component definition to provide fallback values.`
|
|
112
|
+
);
|
|
113
|
+
resolvedFastVS = {};
|
|
114
|
+
resolvedCf = {};
|
|
115
|
+
}
|
|
116
|
+
const signalVS = makeSignals$2(resolvedFastVS);
|
|
117
|
+
const compCore = interactiveConstructor(
|
|
118
|
+
signalProps,
|
|
119
|
+
refs,
|
|
120
|
+
signalVS,
|
|
121
|
+
resolvedCf,
|
|
122
|
+
...pluginResolvedContexts
|
|
123
|
+
);
|
|
124
|
+
const originalRender = compCore.render;
|
|
125
|
+
compCore.render = () => {
|
|
126
|
+
return { ...resolvedFastVS, ...originalRender() };
|
|
127
|
+
};
|
|
128
|
+
return compCore;
|
|
96
129
|
};
|
|
97
|
-
return component.makeJayComponent(preRender, wrappedConstructor, ...
|
|
130
|
+
return component.makeJayComponent(preRender, wrappedConstructor, ...resolvedContexts);
|
|
98
131
|
}
|
|
99
|
-
function makeSignals(obj) {
|
|
132
|
+
function makeSignals$1(obj) {
|
|
100
133
|
return Object.keys(obj).reduce((signals, key) => {
|
|
101
134
|
signals[key] = component.createSignal(obj[key]);
|
|
102
135
|
return signals;
|
|
@@ -112,21 +145,93 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
112
145
|
if (headlessInstanceCarryForwards)
|
|
113
146
|
delete fastCarryForward.__headlessInstances;
|
|
114
147
|
const comp = (props, refs, ...contexts) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
const componentContext = runtime.useContext(component.COMPONENT_CONTEXT);
|
|
149
|
+
const instancesData = {
|
|
150
|
+
viewStates: headlessInstanceViewStates || {},
|
|
151
|
+
carryForwards: headlessInstanceCarryForwards || {}
|
|
152
|
+
};
|
|
153
|
+
componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
|
|
154
|
+
const instances = interactiveParts.map(
|
|
155
|
+
(part) => {
|
|
156
|
+
const partRefs = part.key ? refs[part.key] : refs;
|
|
157
|
+
let partContexts;
|
|
158
|
+
if (hasFastRendering) {
|
|
159
|
+
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
160
|
+
const partFastViewState = partViewState ? makeSignals$1(partViewState) : makeSignals$1({});
|
|
161
|
+
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
162
|
+
partContexts = [
|
|
163
|
+
partFastViewState,
|
|
164
|
+
partCarryForward,
|
|
165
|
+
...contexts.splice(0, part.contextMarkers.length)
|
|
166
|
+
];
|
|
167
|
+
} else {
|
|
168
|
+
partContexts = [...contexts.splice(0, part.contextMarkers.length)];
|
|
169
|
+
}
|
|
170
|
+
return [part.key, part.comp(props, partRefs, ...partContexts)];
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
render: () => {
|
|
175
|
+
let viewState = defaultViewState;
|
|
176
|
+
instances.forEach(([key, instance]) => {
|
|
177
|
+
const rendered = component.materializeViewState(instance.render());
|
|
178
|
+
if (key) {
|
|
179
|
+
viewState[key] = deepMergeViewStates$1(
|
|
180
|
+
defaultViewState[key],
|
|
181
|
+
rendered,
|
|
182
|
+
trackByMap,
|
|
183
|
+
key
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
viewState = deepMergeViewStates$1(
|
|
187
|
+
viewState,
|
|
188
|
+
rendered,
|
|
189
|
+
trackByMap
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return viewState;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
198
|
+
return [...cm, ...part.contextMarkers];
|
|
199
|
+
}, []);
|
|
200
|
+
return component.makeJayComponent(
|
|
201
|
+
preRender,
|
|
202
|
+
comp,
|
|
203
|
+
...contextMarkers
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
function makeSignals(obj) {
|
|
207
|
+
return Object.keys(obj).reduce((signals, key) => {
|
|
208
|
+
signals[key] = component.createSignal(obj[key]);
|
|
209
|
+
return signals;
|
|
210
|
+
}, {});
|
|
211
|
+
}
|
|
212
|
+
function hydrateCompositeJayComponent(hydratePreRender, defaultViewState, fastCarryForward, parts, trackByMap = {}, rootElement) {
|
|
213
|
+
const interactiveParts = parts.filter((part) => part.comp !== void 0);
|
|
214
|
+
const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
|
|
215
|
+
const headlessInstanceViewStates = defaultViewState?.__headlessInstances;
|
|
216
|
+
const headlessInstanceCarryForwards = fastCarryForward?.__headlessInstances;
|
|
217
|
+
if (headlessInstanceViewStates)
|
|
218
|
+
delete defaultViewState.__headlessInstances;
|
|
219
|
+
if (headlessInstanceCarryForwards)
|
|
220
|
+
delete fastCarryForward.__headlessInstances;
|
|
221
|
+
const comp = (props, refs, ...contexts) => {
|
|
222
|
+
const componentContext = runtime.useContext(component.COMPONENT_CONTEXT);
|
|
223
|
+
const instancesData = {
|
|
224
|
+
viewStates: headlessInstanceViewStates || {},
|
|
225
|
+
carryForwards: headlessInstanceCarryForwards || {}
|
|
226
|
+
};
|
|
227
|
+
componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
|
|
123
228
|
const instances = interactiveParts.map(
|
|
124
229
|
(part) => {
|
|
125
230
|
const partRefs = part.key ? refs[part.key] : refs;
|
|
126
231
|
let partContexts;
|
|
127
232
|
if (hasFastRendering) {
|
|
128
233
|
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
129
|
-
const partFastViewState = partViewState ? makeSignals(partViewState) :
|
|
234
|
+
const partFastViewState = partViewState ? makeSignals(partViewState) : makeSignals({});
|
|
130
235
|
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
131
236
|
partContexts = [
|
|
132
237
|
partFastViewState,
|
|
@@ -166,6 +271,7 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
166
271
|
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
167
272
|
return [...cm, ...part.contextMarkers];
|
|
168
273
|
}, []);
|
|
274
|
+
const preRender = (options) => hydratePreRender(rootElement, options);
|
|
169
275
|
return component.makeJayComponent(
|
|
170
276
|
preRender,
|
|
171
277
|
comp,
|
|
@@ -357,14 +463,13 @@ function collectInteractionsRecursive(refs, interactions) {
|
|
|
357
463
|
continue;
|
|
358
464
|
if (refImpl.elements && refImpl.elements instanceof Set) {
|
|
359
465
|
for (const elem of refImpl.elements) {
|
|
360
|
-
if (elem.element) {
|
|
466
|
+
if (elem.element && !isDisabled(elem.element)) {
|
|
361
467
|
interactions.push({
|
|
362
468
|
refName,
|
|
363
469
|
coordinate: elem.coordinate || [refName],
|
|
364
470
|
element: elem.element,
|
|
365
|
-
elementType: getElementType(elem.element),
|
|
366
471
|
supportedEvents: getSupportedEvents(elem.element),
|
|
367
|
-
|
|
472
|
+
description: elem.description
|
|
368
473
|
});
|
|
369
474
|
}
|
|
370
475
|
}
|
|
@@ -383,8 +488,12 @@ function isNestedRefsObject(obj) {
|
|
|
383
488
|
return false;
|
|
384
489
|
return true;
|
|
385
490
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
491
|
+
function isDisabled(element) {
|
|
492
|
+
if ("disabled" in element && element.disabled) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
const fieldset = element.closest?.("fieldset:disabled");
|
|
496
|
+
return !!fieldset;
|
|
388
497
|
}
|
|
389
498
|
function getSupportedEvents(element) {
|
|
390
499
|
const base = ["click", "focus", "blur"];
|
|
@@ -408,11 +517,45 @@ function getSupportedEvents(element) {
|
|
|
408
517
|
}
|
|
409
518
|
return base;
|
|
410
519
|
}
|
|
520
|
+
function groupInteractions(raw) {
|
|
521
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const item of raw) {
|
|
523
|
+
let group = byRef.get(item.refName);
|
|
524
|
+
if (!group) {
|
|
525
|
+
group = [];
|
|
526
|
+
byRef.set(item.refName, group);
|
|
527
|
+
}
|
|
528
|
+
group.push(item);
|
|
529
|
+
}
|
|
530
|
+
return Array.from(byRef.entries()).map(([refName, items]) => ({
|
|
531
|
+
refName,
|
|
532
|
+
description: items[0].description,
|
|
533
|
+
items: items.map(toInstance)
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
function toInstance(raw) {
|
|
537
|
+
return {
|
|
538
|
+
coordinate: raw.coordinate,
|
|
539
|
+
element: raw.element,
|
|
540
|
+
events: relevantEvents(raw)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function relevantEvents(raw) {
|
|
544
|
+
const el = raw.element;
|
|
545
|
+
if (el instanceof HTMLButtonElement || el instanceof HTMLAnchorElement) {
|
|
546
|
+
return ["click"];
|
|
547
|
+
}
|
|
548
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
549
|
+
return raw.supportedEvents.filter((e) => e === "input" || e === "change");
|
|
550
|
+
}
|
|
551
|
+
return raw.supportedEvents;
|
|
552
|
+
}
|
|
411
553
|
const VIEW_STATE_CHANGE = "viewStateChange";
|
|
412
554
|
class AutomationAgent {
|
|
413
555
|
constructor(component2, options) {
|
|
414
556
|
__publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
415
|
-
__publicField2(this, "
|
|
557
|
+
__publicField2(this, "cachedRaw", null);
|
|
558
|
+
__publicField2(this, "cachedGrouped", null);
|
|
416
559
|
__publicField2(this, "viewStateHandler", null);
|
|
417
560
|
__publicField2(this, "mergedViewState");
|
|
418
561
|
__publicField2(this, "initialSlowViewState");
|
|
@@ -431,7 +574,8 @@ class AutomationAgent {
|
|
|
431
574
|
}
|
|
432
575
|
subscribeToUpdates() {
|
|
433
576
|
this.viewStateHandler = () => {
|
|
434
|
-
this.
|
|
577
|
+
this.cachedRaw = null;
|
|
578
|
+
this.cachedGrouped = null;
|
|
435
579
|
if (this.initialSlowViewState && this.trackByMap) {
|
|
436
580
|
this.mergedViewState = deepMergeViewStates(
|
|
437
581
|
this.initialSlowViewState,
|
|
@@ -449,33 +593,43 @@ class AutomationAgent {
|
|
|
449
593
|
const state = this.getPageState();
|
|
450
594
|
this.stateListeners.forEach((callback) => callback(state));
|
|
451
595
|
}
|
|
452
|
-
|
|
453
|
-
if (!this.
|
|
454
|
-
|
|
596
|
+
getGrouped() {
|
|
597
|
+
if (!this.cachedGrouped) {
|
|
598
|
+
if (!this.cachedRaw) {
|
|
599
|
+
this.cachedRaw = collectInteractions(this.component.element?.refs);
|
|
600
|
+
}
|
|
601
|
+
this.cachedGrouped = groupInteractions(this.cachedRaw);
|
|
455
602
|
}
|
|
603
|
+
return this.cachedGrouped;
|
|
604
|
+
}
|
|
605
|
+
getPageState() {
|
|
456
606
|
return {
|
|
457
607
|
// Use merged state if available (slow+fast), otherwise component's viewState
|
|
458
608
|
viewState: this.mergedViewState || this.component.viewState,
|
|
459
|
-
interactions: this.
|
|
609
|
+
interactions: this.getGrouped(),
|
|
460
610
|
customEvents: this.getCustomEvents()
|
|
461
611
|
};
|
|
462
612
|
}
|
|
463
613
|
triggerEvent(eventType, coordinate, eventData) {
|
|
464
|
-
const
|
|
465
|
-
if (!
|
|
614
|
+
const instance = this.getInteraction(coordinate);
|
|
615
|
+
if (!instance) {
|
|
466
616
|
throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
|
|
467
617
|
}
|
|
468
618
|
const event = new Event(eventType, { bubbles: true });
|
|
469
619
|
if (eventData) {
|
|
470
620
|
Object.assign(event, eventData);
|
|
471
621
|
}
|
|
472
|
-
|
|
622
|
+
instance.element.dispatchEvent(event);
|
|
473
623
|
}
|
|
474
624
|
getInteraction(coordinate) {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
625
|
+
for (const group of this.getGrouped()) {
|
|
626
|
+
for (const instance of group.items) {
|
|
627
|
+
if (instance.coordinate.length === coordinate.length && instance.coordinate.every((c, idx) => c === coordinate[idx])) {
|
|
628
|
+
return instance;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return void 0;
|
|
479
633
|
}
|
|
480
634
|
onStateChange(callback) {
|
|
481
635
|
this.stateListeners.add(callback);
|
|
@@ -508,7 +662,8 @@ class AutomationAgent {
|
|
|
508
662
|
this.viewStateHandler = null;
|
|
509
663
|
}
|
|
510
664
|
this.stateListeners.clear();
|
|
511
|
-
this.
|
|
665
|
+
this.cachedRaw = null;
|
|
666
|
+
this.cachedGrouped = null;
|
|
512
667
|
}
|
|
513
668
|
}
|
|
514
669
|
function wrapWithAutomation(component2, options) {
|
|
@@ -520,6 +675,7 @@ exports.AUTOMATION_CONTEXT = AUTOMATION_CONTEXT;
|
|
|
520
675
|
exports.ActionError = ActionError;
|
|
521
676
|
exports.HEADLESS_INSTANCES = HEADLESS_INSTANCES;
|
|
522
677
|
exports.createActionCaller = createActionCaller;
|
|
678
|
+
exports.hydrateCompositeJayComponent = hydrateCompositeJayComponent;
|
|
523
679
|
exports.makeCompositeJayComponent = makeCompositeJayComponent;
|
|
524
680
|
exports.makeHeadlessInstanceComponent = makeHeadlessInstanceComponent;
|
|
525
681
|
exports.setActionCallerOptions = setActionCallerOptions;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _jay_framework_component from '@jay-framework/component';
|
|
2
|
-
import { ComponentConstructor, ContextMarkers, JayComponentCore } from '@jay-framework/component';
|
|
3
|
-
import { JayElement, PreRenderElement, ContextMarker } from '@jay-framework/runtime';
|
|
2
|
+
import { ComponentConstructor, ContextMarkers, JayComponentCore, ConcreteJayComponent } from '@jay-framework/component';
|
|
3
|
+
import { JayElement, PreRenderElement, RenderElementOptions, RenderElement, ContextMarker } from '@jay-framework/runtime';
|
|
4
4
|
import { TrackByMap } from '@jay-framework/view-state-merge';
|
|
5
5
|
export { AUTOMATION_CONTEXT, AutomationAPI, AutomationWrappedComponent, Coordinate, Interaction, PageState, wrapWithAutomation } from '@jay-framework/runtime-automation';
|
|
6
6
|
|
|
@@ -17,6 +17,15 @@ interface CompositePart {
|
|
|
17
17
|
|
|
18
18
|
declare function makeCompositeJayComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, CompCore extends JayComponentCore<PropsT, ViewState>>(preRender: PreRenderElement<ViewState, Refs, JayElementT>, defaultViewState: ViewState, fastCarryForward: object, parts: Array<CompositePart>, trackByMap?: TrackByMap): (props: PropsT) => _jay_framework_component.ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Hydrate variant of makeCompositeJayComponent.
|
|
22
|
+
*
|
|
23
|
+
* Instead of creating new DOM, this adopts the server-rendered DOM by using
|
|
24
|
+
* the hydrate target's function signature: (rootElement, options?) => [Refs, RenderElement].
|
|
25
|
+
* The rootElement is the SSR-rendered DOM already present in the page.
|
|
26
|
+
*/
|
|
27
|
+
declare function hydrateCompositeJayComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, CompCore extends JayComponentCore<PropsT, ViewState>>(hydratePreRender: (rootElement: Element, options?: RenderElementOptions) => [Refs, RenderElement<ViewState, Refs, JayElementT>], defaultViewState: ViewState, fastCarryForward: object, parts: Array<CompositePart>, trackByMap: TrackByMap, rootElement: Element): (props: PropsT) => _jay_framework_component.ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* Client-side context and helpers for headless component instances.
|
|
22
31
|
*
|
|
@@ -53,10 +62,25 @@ declare const HEADLESS_INSTANCES: ContextMarker<HeadlessInstancesData>;
|
|
|
53
62
|
*
|
|
54
63
|
* @param preRender - The inline template's render function
|
|
55
64
|
* @param interactiveConstructor - The plugin's interactive constructor
|
|
56
|
-
* @param coordinateKey -
|
|
65
|
+
* @param coordinateKey - Static coordinate key (e.g., "product-card:0") or a
|
|
66
|
+
* factory function for forEach instances that receives the current dataIds
|
|
67
|
+
* (accumulated trackBy values from ancestor forEach loops) and returns the key.
|
|
57
68
|
* @param pluginContexts - Additional context markers from the plugin (if any)
|
|
58
69
|
*/
|
|
59
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Component definition shape expected by makeHeadlessInstanceComponent.
|
|
72
|
+
* `clientDefaults` receives raw props (not signal-wrapped) since it's called
|
|
73
|
+
* before the component construction context is set up.
|
|
74
|
+
*/
|
|
75
|
+
interface HeadlessComponentDef {
|
|
76
|
+
comp: ComponentConstructor<any, any, any, any, any>;
|
|
77
|
+
contexts?: ContextMarkers<any>;
|
|
78
|
+
clientDefaults?: (props: any) => {
|
|
79
|
+
viewState: any;
|
|
80
|
+
carryForward?: any;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
declare function makeHeadlessInstanceComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, CompCore extends JayComponentCore<PropsT, ViewState>>(preRender: PreRenderElement<ViewState, Refs, JayElementT>, componentDef: HeadlessComponentDef, coordinateKey: string | ((dataIds: string[]) => string)): (props: PropsT) => ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
|
|
60
84
|
|
|
61
85
|
/**
|
|
62
86
|
* Client-side action caller for Jay Stack.
|
|
@@ -123,4 +147,4 @@ declare function setActionCallerOptions(options: ActionCallerOptions): void;
|
|
|
123
147
|
*/
|
|
124
148
|
declare function createActionCaller<Input, Output>(actionName: string, method?: HttpMethod): (input: Input) => Promise<Output>;
|
|
125
149
|
|
|
126
|
-
export { type ActionCallerOptions, ActionError, type CompositePart, HEADLESS_INSTANCES, type HeadlessInstancesData, type HttpMethod, createActionCaller, makeCompositeJayComponent, makeHeadlessInstanceComponent, setActionCallerOptions };
|
|
150
|
+
export { type ActionCallerOptions, ActionError, type CompositePart, HEADLESS_INSTANCES, type HeadlessComponentDef, type HeadlessInstancesData, type HttpMethod, createActionCaller, hydrateCompositeJayComponent, makeCompositeJayComponent, makeHeadlessInstanceComponent, setActionCallerOptions };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __publicField = (obj, key, value) => {
|
|
|
5
5
|
return value;
|
|
6
6
|
};
|
|
7
7
|
import { makeJayComponent, createSignal, COMPONENT_CONTEXT, materializeViewState } from "@jay-framework/component";
|
|
8
|
-
import { createJayContext, useContext } from "@jay-framework/runtime";
|
|
8
|
+
import { createJayContext, useContext, currentConstructionContext } from "@jay-framework/runtime";
|
|
9
9
|
function deepMergeViewStates$1(base, overlay, trackByMap, path = "") {
|
|
10
10
|
if (!base && !overlay)
|
|
11
11
|
return {};
|
|
@@ -78,23 +78,56 @@ function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMa
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
const HEADLESS_INSTANCES = createJayContext();
|
|
81
|
-
function makeSignals$
|
|
81
|
+
function makeSignals$2(obj) {
|
|
82
82
|
return Object.keys(obj).reduce((signals, key) => {
|
|
83
83
|
signals[key] = createSignal(obj[key]);
|
|
84
84
|
return signals;
|
|
85
85
|
}, {});
|
|
86
86
|
}
|
|
87
|
-
function makeHeadlessInstanceComponent(preRender,
|
|
88
|
-
const
|
|
87
|
+
function makeHeadlessInstanceComponent(preRender, componentDef, coordinateKey) {
|
|
88
|
+
const interactiveConstructor = componentDef.comp;
|
|
89
|
+
const resolvedContexts = componentDef.contexts ?? [];
|
|
90
|
+
const clientDefaults = componentDef.clientDefaults;
|
|
91
|
+
const wrappedConstructor = (signalProps, refs, ...pluginResolvedContexts) => {
|
|
89
92
|
const instanceData = useContext(HEADLESS_INSTANCES);
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
93
|
+
const resolvedKey = typeof coordinateKey === "function" ? coordinateKey(currentConstructionContext()?.dataIds ?? []) : coordinateKey;
|
|
94
|
+
const suffixKey = resolvedKey.includes("/") && resolvedKey.includes(":") ? resolvedKey.split("/").find((s) => s.includes(":")) ?? resolvedKey : resolvedKey;
|
|
95
|
+
const fastVS = instanceData?.viewStates?.[resolvedKey] ?? instanceData?.viewStates?.[suffixKey];
|
|
96
|
+
const cf = instanceData?.carryForwards?.[resolvedKey] ?? instanceData?.carryForwards?.[suffixKey];
|
|
97
|
+
let resolvedFastVS;
|
|
98
|
+
let resolvedCf;
|
|
99
|
+
if (fastVS) {
|
|
100
|
+
resolvedFastVS = fastVS;
|
|
101
|
+
resolvedCf = cf || {};
|
|
102
|
+
} else if (clientDefaults) {
|
|
103
|
+
const rawProps = signalProps.props();
|
|
104
|
+
const defaults = clientDefaults(rawProps);
|
|
105
|
+
resolvedFastVS = defaults.viewState;
|
|
106
|
+
resolvedCf = defaults.carryForward ?? {};
|
|
107
|
+
} else {
|
|
108
|
+
console.warn(
|
|
109
|
+
`[Jay] Headless instance "${resolvedKey}" has no server data and no clientDefaults. Add .withClientDefaults() to the component definition to provide fallback values.`
|
|
110
|
+
);
|
|
111
|
+
resolvedFastVS = {};
|
|
112
|
+
resolvedCf = {};
|
|
113
|
+
}
|
|
114
|
+
const signalVS = makeSignals$2(resolvedFastVS);
|
|
115
|
+
const compCore = interactiveConstructor(
|
|
116
|
+
signalProps,
|
|
117
|
+
refs,
|
|
118
|
+
signalVS,
|
|
119
|
+
resolvedCf,
|
|
120
|
+
...pluginResolvedContexts
|
|
121
|
+
);
|
|
122
|
+
const originalRender = compCore.render;
|
|
123
|
+
compCore.render = () => {
|
|
124
|
+
return { ...resolvedFastVS, ...originalRender() };
|
|
125
|
+
};
|
|
126
|
+
return compCore;
|
|
94
127
|
};
|
|
95
|
-
return makeJayComponent(preRender, wrappedConstructor, ...
|
|
128
|
+
return makeJayComponent(preRender, wrappedConstructor, ...resolvedContexts);
|
|
96
129
|
}
|
|
97
|
-
function makeSignals(obj) {
|
|
130
|
+
function makeSignals$1(obj) {
|
|
98
131
|
return Object.keys(obj).reduce((signals, key) => {
|
|
99
132
|
signals[key] = createSignal(obj[key]);
|
|
100
133
|
return signals;
|
|
@@ -110,21 +143,93 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
110
143
|
if (headlessInstanceCarryForwards)
|
|
111
144
|
delete fastCarryForward.__headlessInstances;
|
|
112
145
|
const comp = (props, refs, ...contexts) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
const componentContext = useContext(COMPONENT_CONTEXT);
|
|
147
|
+
const instancesData = {
|
|
148
|
+
viewStates: headlessInstanceViewStates || {},
|
|
149
|
+
carryForwards: headlessInstanceCarryForwards || {}
|
|
150
|
+
};
|
|
151
|
+
componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
|
|
152
|
+
const instances = interactiveParts.map(
|
|
153
|
+
(part) => {
|
|
154
|
+
const partRefs = part.key ? refs[part.key] : refs;
|
|
155
|
+
let partContexts;
|
|
156
|
+
if (hasFastRendering) {
|
|
157
|
+
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
158
|
+
const partFastViewState = partViewState ? makeSignals$1(partViewState) : makeSignals$1({});
|
|
159
|
+
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
160
|
+
partContexts = [
|
|
161
|
+
partFastViewState,
|
|
162
|
+
partCarryForward,
|
|
163
|
+
...contexts.splice(0, part.contextMarkers.length)
|
|
164
|
+
];
|
|
165
|
+
} else {
|
|
166
|
+
partContexts = [...contexts.splice(0, part.contextMarkers.length)];
|
|
167
|
+
}
|
|
168
|
+
return [part.key, part.comp(props, partRefs, ...partContexts)];
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
return {
|
|
172
|
+
render: () => {
|
|
173
|
+
let viewState = defaultViewState;
|
|
174
|
+
instances.forEach(([key, instance]) => {
|
|
175
|
+
const rendered = materializeViewState(instance.render());
|
|
176
|
+
if (key) {
|
|
177
|
+
viewState[key] = deepMergeViewStates$1(
|
|
178
|
+
defaultViewState[key],
|
|
179
|
+
rendered,
|
|
180
|
+
trackByMap,
|
|
181
|
+
key
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
viewState = deepMergeViewStates$1(
|
|
185
|
+
viewState,
|
|
186
|
+
rendered,
|
|
187
|
+
trackByMap
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return viewState;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
196
|
+
return [...cm, ...part.contextMarkers];
|
|
197
|
+
}, []);
|
|
198
|
+
return makeJayComponent(
|
|
199
|
+
preRender,
|
|
200
|
+
comp,
|
|
201
|
+
...contextMarkers
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function makeSignals(obj) {
|
|
205
|
+
return Object.keys(obj).reduce((signals, key) => {
|
|
206
|
+
signals[key] = createSignal(obj[key]);
|
|
207
|
+
return signals;
|
|
208
|
+
}, {});
|
|
209
|
+
}
|
|
210
|
+
function hydrateCompositeJayComponent(hydratePreRender, defaultViewState, fastCarryForward, parts, trackByMap = {}, rootElement) {
|
|
211
|
+
const interactiveParts = parts.filter((part) => part.comp !== void 0);
|
|
212
|
+
const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
|
|
213
|
+
const headlessInstanceViewStates = defaultViewState?.__headlessInstances;
|
|
214
|
+
const headlessInstanceCarryForwards = fastCarryForward?.__headlessInstances;
|
|
215
|
+
if (headlessInstanceViewStates)
|
|
216
|
+
delete defaultViewState.__headlessInstances;
|
|
217
|
+
if (headlessInstanceCarryForwards)
|
|
218
|
+
delete fastCarryForward.__headlessInstances;
|
|
219
|
+
const comp = (props, refs, ...contexts) => {
|
|
220
|
+
const componentContext = useContext(COMPONENT_CONTEXT);
|
|
221
|
+
const instancesData = {
|
|
222
|
+
viewStates: headlessInstanceViewStates || {},
|
|
223
|
+
carryForwards: headlessInstanceCarryForwards || {}
|
|
224
|
+
};
|
|
225
|
+
componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
|
|
121
226
|
const instances = interactiveParts.map(
|
|
122
227
|
(part) => {
|
|
123
228
|
const partRefs = part.key ? refs[part.key] : refs;
|
|
124
229
|
let partContexts;
|
|
125
230
|
if (hasFastRendering) {
|
|
126
231
|
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
127
|
-
const partFastViewState = partViewState ? makeSignals(partViewState) :
|
|
232
|
+
const partFastViewState = partViewState ? makeSignals(partViewState) : makeSignals({});
|
|
128
233
|
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
129
234
|
partContexts = [
|
|
130
235
|
partFastViewState,
|
|
@@ -164,6 +269,7 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
164
269
|
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
165
270
|
return [...cm, ...part.contextMarkers];
|
|
166
271
|
}, []);
|
|
272
|
+
const preRender = (options) => hydratePreRender(rootElement, options);
|
|
167
273
|
return makeJayComponent(
|
|
168
274
|
preRender,
|
|
169
275
|
comp,
|
|
@@ -355,14 +461,13 @@ function collectInteractionsRecursive(refs, interactions) {
|
|
|
355
461
|
continue;
|
|
356
462
|
if (refImpl.elements && refImpl.elements instanceof Set) {
|
|
357
463
|
for (const elem of refImpl.elements) {
|
|
358
|
-
if (elem.element) {
|
|
464
|
+
if (elem.element && !isDisabled(elem.element)) {
|
|
359
465
|
interactions.push({
|
|
360
466
|
refName,
|
|
361
467
|
coordinate: elem.coordinate || [refName],
|
|
362
468
|
element: elem.element,
|
|
363
|
-
elementType: getElementType(elem.element),
|
|
364
469
|
supportedEvents: getSupportedEvents(elem.element),
|
|
365
|
-
|
|
470
|
+
description: elem.description
|
|
366
471
|
});
|
|
367
472
|
}
|
|
368
473
|
}
|
|
@@ -381,8 +486,12 @@ function isNestedRefsObject(obj) {
|
|
|
381
486
|
return false;
|
|
382
487
|
return true;
|
|
383
488
|
}
|
|
384
|
-
function
|
|
385
|
-
|
|
489
|
+
function isDisabled(element) {
|
|
490
|
+
if ("disabled" in element && element.disabled) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
const fieldset = element.closest?.("fieldset:disabled");
|
|
494
|
+
return !!fieldset;
|
|
386
495
|
}
|
|
387
496
|
function getSupportedEvents(element) {
|
|
388
497
|
const base = ["click", "focus", "blur"];
|
|
@@ -406,11 +515,45 @@ function getSupportedEvents(element) {
|
|
|
406
515
|
}
|
|
407
516
|
return base;
|
|
408
517
|
}
|
|
518
|
+
function groupInteractions(raw) {
|
|
519
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const item of raw) {
|
|
521
|
+
let group = byRef.get(item.refName);
|
|
522
|
+
if (!group) {
|
|
523
|
+
group = [];
|
|
524
|
+
byRef.set(item.refName, group);
|
|
525
|
+
}
|
|
526
|
+
group.push(item);
|
|
527
|
+
}
|
|
528
|
+
return Array.from(byRef.entries()).map(([refName, items]) => ({
|
|
529
|
+
refName,
|
|
530
|
+
description: items[0].description,
|
|
531
|
+
items: items.map(toInstance)
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
function toInstance(raw) {
|
|
535
|
+
return {
|
|
536
|
+
coordinate: raw.coordinate,
|
|
537
|
+
element: raw.element,
|
|
538
|
+
events: relevantEvents(raw)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function relevantEvents(raw) {
|
|
542
|
+
const el = raw.element;
|
|
543
|
+
if (el instanceof HTMLButtonElement || el instanceof HTMLAnchorElement) {
|
|
544
|
+
return ["click"];
|
|
545
|
+
}
|
|
546
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
547
|
+
return raw.supportedEvents.filter((e) => e === "input" || e === "change");
|
|
548
|
+
}
|
|
549
|
+
return raw.supportedEvents;
|
|
550
|
+
}
|
|
409
551
|
const VIEW_STATE_CHANGE = "viewStateChange";
|
|
410
552
|
class AutomationAgent {
|
|
411
553
|
constructor(component, options) {
|
|
412
554
|
__publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
413
|
-
__publicField2(this, "
|
|
555
|
+
__publicField2(this, "cachedRaw", null);
|
|
556
|
+
__publicField2(this, "cachedGrouped", null);
|
|
414
557
|
__publicField2(this, "viewStateHandler", null);
|
|
415
558
|
__publicField2(this, "mergedViewState");
|
|
416
559
|
__publicField2(this, "initialSlowViewState");
|
|
@@ -429,7 +572,8 @@ class AutomationAgent {
|
|
|
429
572
|
}
|
|
430
573
|
subscribeToUpdates() {
|
|
431
574
|
this.viewStateHandler = () => {
|
|
432
|
-
this.
|
|
575
|
+
this.cachedRaw = null;
|
|
576
|
+
this.cachedGrouped = null;
|
|
433
577
|
if (this.initialSlowViewState && this.trackByMap) {
|
|
434
578
|
this.mergedViewState = deepMergeViewStates(
|
|
435
579
|
this.initialSlowViewState,
|
|
@@ -447,33 +591,43 @@ class AutomationAgent {
|
|
|
447
591
|
const state = this.getPageState();
|
|
448
592
|
this.stateListeners.forEach((callback) => callback(state));
|
|
449
593
|
}
|
|
450
|
-
|
|
451
|
-
if (!this.
|
|
452
|
-
|
|
594
|
+
getGrouped() {
|
|
595
|
+
if (!this.cachedGrouped) {
|
|
596
|
+
if (!this.cachedRaw) {
|
|
597
|
+
this.cachedRaw = collectInteractions(this.component.element?.refs);
|
|
598
|
+
}
|
|
599
|
+
this.cachedGrouped = groupInteractions(this.cachedRaw);
|
|
453
600
|
}
|
|
601
|
+
return this.cachedGrouped;
|
|
602
|
+
}
|
|
603
|
+
getPageState() {
|
|
454
604
|
return {
|
|
455
605
|
// Use merged state if available (slow+fast), otherwise component's viewState
|
|
456
606
|
viewState: this.mergedViewState || this.component.viewState,
|
|
457
|
-
interactions: this.
|
|
607
|
+
interactions: this.getGrouped(),
|
|
458
608
|
customEvents: this.getCustomEvents()
|
|
459
609
|
};
|
|
460
610
|
}
|
|
461
611
|
triggerEvent(eventType, coordinate, eventData) {
|
|
462
|
-
const
|
|
463
|
-
if (!
|
|
612
|
+
const instance = this.getInteraction(coordinate);
|
|
613
|
+
if (!instance) {
|
|
464
614
|
throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
|
|
465
615
|
}
|
|
466
616
|
const event = new Event(eventType, { bubbles: true });
|
|
467
617
|
if (eventData) {
|
|
468
618
|
Object.assign(event, eventData);
|
|
469
619
|
}
|
|
470
|
-
|
|
620
|
+
instance.element.dispatchEvent(event);
|
|
471
621
|
}
|
|
472
622
|
getInteraction(coordinate) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
623
|
+
for (const group of this.getGrouped()) {
|
|
624
|
+
for (const instance of group.items) {
|
|
625
|
+
if (instance.coordinate.length === coordinate.length && instance.coordinate.every((c, idx) => c === coordinate[idx])) {
|
|
626
|
+
return instance;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return void 0;
|
|
477
631
|
}
|
|
478
632
|
onStateChange(callback) {
|
|
479
633
|
this.stateListeners.add(callback);
|
|
@@ -506,7 +660,8 @@ class AutomationAgent {
|
|
|
506
660
|
this.viewStateHandler = null;
|
|
507
661
|
}
|
|
508
662
|
this.stateListeners.clear();
|
|
509
|
-
this.
|
|
663
|
+
this.cachedRaw = null;
|
|
664
|
+
this.cachedGrouped = null;
|
|
510
665
|
}
|
|
511
666
|
}
|
|
512
667
|
function wrapWithAutomation(component, options) {
|
|
@@ -519,6 +674,7 @@ export {
|
|
|
519
674
|
ActionError,
|
|
520
675
|
HEADLESS_INSTANCES,
|
|
521
676
|
createActionCaller,
|
|
677
|
+
hydrateCompositeJayComponent,
|
|
522
678
|
makeCompositeJayComponent,
|
|
523
679
|
makeHeadlessInstanceComponent,
|
|
524
680
|
setActionCallerOptions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/stack-client-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jay-framework/component": "^0.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.
|
|
32
|
-
"@jay-framework/runtime": "^0.
|
|
33
|
-
"@jay-framework/runtime-automation": "^0.
|
|
34
|
-
"@jay-framework/view-state-merge": "^0.
|
|
30
|
+
"@jay-framework/component": "^0.14.0",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.14.0",
|
|
32
|
+
"@jay-framework/runtime": "^0.14.0",
|
|
33
|
+
"@jay-framework/runtime-automation": "^0.14.0",
|
|
34
|
+
"@jay-framework/view-state-merge": "^0.14.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@jay-framework/dev-environment": "^0.
|
|
38
|
-
"@jay-framework/jay-cli": "^0.
|
|
37
|
+
"@jay-framework/dev-environment": "^0.14.0",
|
|
38
|
+
"@jay-framework/jay-cli": "^0.14.0",
|
|
39
39
|
"@types/express": "^5.0.2",
|
|
40
40
|
"@types/node": "^22.15.21",
|
|
41
41
|
"nodemon": "^3.0.3",
|