@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 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$1(obj) {
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, interactiveConstructor, coordinateKey, pluginContexts = []) {
90
- const wrappedConstructor = (props, refs, ...pluginResolvedContexts) => {
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 fastVS = instanceData?.viewStates?.[coordinateKey];
93
- const cf = instanceData?.carryForwards?.[coordinateKey] || {};
94
- const signalVS = fastVS ? makeSignals$1(fastVS) : void 0;
95
- return interactiveConstructor(props, refs, signalVS, cf, ...pluginResolvedContexts);
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, ...pluginContexts);
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
- if (headlessInstanceViewStates || headlessInstanceCarryForwards) {
116
- const componentContext = runtime.useContext(component.COMPONENT_CONTEXT);
117
- const instancesData = {
118
- viewStates: headlessInstanceViewStates || {},
119
- carryForwards: headlessInstanceCarryForwards || {}
120
- };
121
- componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
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) : void 0;
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
- itemContext: elem.viewState
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 getElementType(element) {
387
- return element.constructor.name;
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, "cachedInteractions", null);
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.cachedInteractions = null;
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
- getPageState() {
453
- if (!this.cachedInteractions) {
454
- this.cachedInteractions = collectInteractions(this.component.element?.refs);
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.cachedInteractions,
609
+ interactions: this.getGrouped(),
460
610
  customEvents: this.getCustomEvents()
461
611
  };
462
612
  }
463
613
  triggerEvent(eventType, coordinate, eventData) {
464
- const interaction = this.getInteraction(coordinate);
465
- if (!interaction) {
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
- interaction.element.dispatchEvent(event);
622
+ instance.element.dispatchEvent(event);
473
623
  }
474
624
  getInteraction(coordinate) {
475
- const state = this.getPageState();
476
- return state.interactions.find(
477
- (i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
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.cachedInteractions = null;
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 - The coordinate key for this instance (e.g., "product-card:0")
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
- 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>, interactiveConstructor: ComponentConstructor<PropsT, Refs, ViewState, any, CompCore>, coordinateKey: string, pluginContexts?: ContextMarkers<any>): any;
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$1(obj) {
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, interactiveConstructor, coordinateKey, pluginContexts = []) {
88
- const wrappedConstructor = (props, refs, ...pluginResolvedContexts) => {
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 fastVS = instanceData?.viewStates?.[coordinateKey];
91
- const cf = instanceData?.carryForwards?.[coordinateKey] || {};
92
- const signalVS = fastVS ? makeSignals$1(fastVS) : void 0;
93
- return interactiveConstructor(props, refs, signalVS, cf, ...pluginResolvedContexts);
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, ...pluginContexts);
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
- if (headlessInstanceViewStates || headlessInstanceCarryForwards) {
114
- const componentContext = useContext(COMPONENT_CONTEXT);
115
- const instancesData = {
116
- viewStates: headlessInstanceViewStates || {},
117
- carryForwards: headlessInstanceCarryForwards || {}
118
- };
119
- componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
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) : void 0;
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
- itemContext: elem.viewState
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 getElementType(element) {
385
- return element.constructor.name;
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, "cachedInteractions", null);
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.cachedInteractions = null;
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
- getPageState() {
451
- if (!this.cachedInteractions) {
452
- this.cachedInteractions = collectInteractions(this.component.element?.refs);
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.cachedInteractions,
607
+ interactions: this.getGrouped(),
458
608
  customEvents: this.getCustomEvents()
459
609
  };
460
610
  }
461
611
  triggerEvent(eventType, coordinate, eventData) {
462
- const interaction = this.getInteraction(coordinate);
463
- if (!interaction) {
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
- interaction.element.dispatchEvent(event);
620
+ instance.element.dispatchEvent(event);
471
621
  }
472
622
  getInteraction(coordinate) {
473
- const state = this.getPageState();
474
- return state.interactions.find(
475
- (i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
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.cachedInteractions = null;
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.12.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.12.0",
31
- "@jay-framework/fullstack-component": "^0.12.0",
32
- "@jay-framework/runtime": "^0.12.0",
33
- "@jay-framework/runtime-automation": "^0.12.0",
34
- "@jay-framework/view-state-merge": "^0.12.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.12.0",
38
- "@jay-framework/jay-cli": "^0.12.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",