@coherent.js/client 1.0.0-beta.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.
@@ -0,0 +1,1791 @@
1
+ /**
2
+ * Client-side hydration utilities for Coherent.js
3
+ *
4
+ * This module provides utilities for hydrating server-rendered HTML
5
+ * with client-side interactivity.
6
+ */
7
+
8
+ // Store for component instances
9
+ const componentInstances = new WeakMap();
10
+
11
+ /**
12
+ * Extract initial state from DOM element data attributes
13
+ *
14
+ * @param {HTMLElement} element - The DOM element
15
+ * @param {Object} options - Hydration options
16
+ * @returns {Object|null} The initial state or null
17
+ */
18
+ function extractInitialState(element, options = {}) {
19
+ // Check if we're in a browser environment
20
+ if (typeof window === 'undefined') {
21
+ return options.initialState || null;
22
+ }
23
+
24
+ // Check if element has getAttribute method
25
+ if (!element || typeof element.getAttribute !== 'function') {
26
+ return options.initialState || null;
27
+ }
28
+
29
+ try {
30
+ // Look for data-coherent-state attribute
31
+ const stateAttr = element.getAttribute('data-coherent-state');
32
+ if (stateAttr) {
33
+ return JSON.parse(stateAttr);
34
+ }
35
+
36
+ // Look for specific state attributes
37
+ const state = {};
38
+ let hasState = false;
39
+
40
+ // Extract common state patterns
41
+ const countAttr = element.getAttribute('data-count');
42
+ if (countAttr !== null) {
43
+ state.count = parseInt(countAttr, 10) || 0;
44
+ hasState = true;
45
+ }
46
+
47
+ const stepAttr = element.getAttribute('data-step');
48
+ if (stepAttr !== null) {
49
+ state.step = parseInt(stepAttr, 10) || 1;
50
+ hasState = true;
51
+ }
52
+
53
+ const todosAttr = element.getAttribute('data-todos');
54
+ if (todosAttr) {
55
+ state.todos = JSON.parse(todosAttr);
56
+ hasState = true;
57
+ }
58
+
59
+ const valueAttr = element.getAttribute('data-value');
60
+ if (valueAttr !== null) {
61
+ state.value = valueAttr;
62
+ hasState = true;
63
+ }
64
+
65
+ // Check for initial props in options
66
+ if (options.initialState) {
67
+ return { ...options.initialState, ...state };
68
+ }
69
+
70
+ return hasState ? state : null;
71
+ } catch (_error) {
72
+ console.warn('Error extracting initial state:', _error);
73
+ return options.initialState || null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Hydrate a DOM element with a Coherent component
79
+ *
80
+ * @param {HTMLElement} element - The DOM element to hydrate
81
+ * @param {Function} component - The Coherent component function
82
+ * @param {Object} props - The props to pass to the component
83
+ * @param {Object} options - Hydration options
84
+ * @returns {Object} The hydrated component instance
85
+ */
86
+ function hydrate(element, component, props = {}, options = {}) {
87
+ // Hydration process initiated
88
+
89
+ if (typeof window === 'undefined') {
90
+ console.warn('Hydration can only be performed in a browser environment');
91
+ return null;
92
+ }
93
+
94
+ // Validate component
95
+ if (typeof component !== 'function') {
96
+ console.error('Hydrate error: component must be a function, received:', typeof component);
97
+ return null;
98
+ }
99
+
100
+ // Check if element is already hydrated
101
+ if (componentInstances.has(element)) {
102
+ const existingInstance = componentInstances.get(element);
103
+ return existingInstance;
104
+ }
105
+
106
+ // Extract initial state from data attributes if available
107
+ const initialState = extractInitialState(element, options);
108
+
109
+ // Create component instance with state management
110
+ const instance = {
111
+ element,
112
+ component,
113
+ props: {...props},
114
+ state: initialState,
115
+ isHydrated: true,
116
+ eventListeners: [],
117
+ options: {...options},
118
+ previousVirtualElement: null,
119
+
120
+ // Update method for re-rendering
121
+ update(newProps) {
122
+ this.props = { ...this.props, ...newProps };
123
+ this.rerender();
124
+ return this; // Return instance for chaining
125
+ },
126
+
127
+ // Re-render the component with current state
128
+ rerender() {
129
+ try {
130
+ // Always use the fallback patching method to preserve hydration
131
+ this.fallbackRerender();
132
+ } catch (_error) {
133
+ console.error('Error during component re-render:', _error);
134
+ }
135
+ },
136
+
137
+ // Fallback re-render method using existing patching
138
+ fallbackRerender() {
139
+ try {
140
+ // Call the component function with current props and state
141
+ const componentProps = { ...this.props, ...(this.state || {}) };
142
+
143
+ // Check if component is a function before calling
144
+ if (typeof this.component !== 'function') {
145
+ console.error('Component is not a function:', this.component);
146
+ return;
147
+ }
148
+
149
+ const newVirtualElement = this.component(componentProps);
150
+
151
+ // Store the previous virtual element for comparison
152
+ if (!this.previousVirtualElement) {
153
+ this.previousVirtualElement = this.virtualElementFromDOM(this.element);
154
+ }
155
+
156
+ // Perform intelligent DOM diffing and patching
157
+ this.patchDOM(this.element, this.previousVirtualElement, newVirtualElement);
158
+
159
+ // Re-attach event listeners for input elements only
160
+ attachFunctionEventListeners(this.element, newVirtualElement, this, { inputsOnly: true });
161
+
162
+ // Store the new virtual element for next comparison
163
+ this.previousVirtualElement = newVirtualElement;
164
+
165
+ // Component re-rendered successfully with fallback
166
+ } catch (_error) {
167
+ console.error('Error during component re-render (fallback):', _error);
168
+ }
169
+ },
170
+
171
+ // Create virtual element representation from existing DOM
172
+ virtualElementFromDOM(domElement) {
173
+ // Check if we're in a browser environment
174
+ if (typeof window === 'undefined' || typeof Node === 'undefined') {
175
+ return null;
176
+ }
177
+
178
+ if (domElement.nodeType === Node.TEXT_NODE) {
179
+ return domElement.textContent;
180
+ }
181
+
182
+ if (domElement.nodeType !== Node.ELEMENT_NODE) {
183
+ return null;
184
+ }
185
+
186
+ const tagName = domElement.tagName.toLowerCase();
187
+ const props = {};
188
+ const children = [];
189
+
190
+ // Extract attributes
191
+ if (domElement.attributes) {
192
+ Array.from(domElement.attributes).forEach(attr => {
193
+ const name = attr.name === 'class' ? 'className' : attr.name;
194
+ props[name] = attr.value;
195
+ });
196
+ }
197
+
198
+ // Extract children
199
+ if (domElement.childNodes) {
200
+ Array.from(domElement.childNodes).forEach(child => {
201
+ if (child.nodeType === Node.TEXT_NODE) {
202
+ const text = child.textContent.trim();
203
+ if (text) children.push(text);
204
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
205
+ const childVNode = this.virtualElementFromDOM(child);
206
+ if (childVNode) children.push(childVNode);
207
+ }
208
+ });
209
+ }
210
+
211
+ if (children.length > 0) {
212
+ props.children = children;
213
+ }
214
+
215
+ return { [tagName]: props };
216
+ },
217
+
218
+ // Intelligent DOM patching with minimal changes
219
+ patchDOM(domElement, oldVNode, newVNode) {
220
+ // Handle text nodes
221
+ if (typeof newVNode === 'string' || typeof newVNode === 'number') {
222
+ const newText = String(newVNode);
223
+ // Check if we're in a browser environment
224
+ if (typeof window === 'undefined' || typeof Node === 'undefined' || typeof document === 'undefined') {
225
+ return;
226
+ }
227
+
228
+ if (domElement.nodeType === Node.TEXT_NODE) {
229
+ if (domElement.textContent !== newText) {
230
+ domElement.textContent = newText;
231
+ }
232
+ } else {
233
+ // Replace element with text node
234
+ const textNode = document.createTextNode(newText);
235
+ if (domElement.parentNode) {
236
+ domElement.parentNode.replaceChild(textNode, domElement);
237
+ }
238
+ }
239
+ return;
240
+ }
241
+
242
+ // Handle null/undefined
243
+ if (!newVNode) {
244
+ domElement.remove();
245
+ return;
246
+ }
247
+
248
+ // Handle arrays
249
+ if (Array.isArray(newVNode)) {
250
+ // This shouldn't happen at the root level, but handle gracefully
251
+ console.warn('Array virtual node at root level');
252
+ return;
253
+ }
254
+
255
+ // Handle element nodes
256
+ const newTagName = Object.keys(newVNode)[0];
257
+
258
+ // Check if tag name changed
259
+ if (domElement.tagName.toLowerCase() !== newTagName.toLowerCase()) {
260
+ // Need to replace the entire element
261
+ // Check if we're in a browser environment
262
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
263
+ return;
264
+ }
265
+
266
+ const newElement = this.createDOMElement(newVNode);
267
+ if (domElement.parentNode) {
268
+ domElement.parentNode.replaceChild(newElement, domElement);
269
+ }
270
+ attachEventListeners(newElement, this);
271
+ return;
272
+ }
273
+
274
+ // Update attributes
275
+ this.patchAttributes(domElement, oldVNode, newVNode);
276
+
277
+ // Update children
278
+ this.patchChildren(domElement, oldVNode, newVNode);
279
+
280
+ // Re-attach event listeners if needed
281
+ attachEventListeners(domElement, this);
282
+ },
283
+
284
+ // Patch element attributes efficiently
285
+ patchAttributes(domElement, oldVNode, newVNode) {
286
+ // Check if we're in a browser environment
287
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
288
+ return;
289
+ }
290
+
291
+ // Check if domElement has required methods
292
+ if (!domElement || typeof domElement.setAttribute !== 'function' || typeof domElement.removeAttribute !== 'function') {
293
+ return;
294
+ }
295
+
296
+ const oldTagName = oldVNode ? Object.keys(oldVNode)[0] : null;
297
+ const newTagName = Object.keys(newVNode)[0];
298
+ const oldProps = oldVNode && oldTagName ? (oldVNode[oldTagName] || {}) : {};
299
+ const newProps = newVNode[newTagName] || {};
300
+
301
+ // Remove old attributes that are no longer present
302
+ Object.keys(oldProps).forEach(key => {
303
+ if (key === 'children' || key === 'text') return;
304
+ if (!(key in newProps)) {
305
+ const attrName = key === 'className' ? 'class' : key;
306
+ domElement.removeAttribute(attrName);
307
+ }
308
+ });
309
+
310
+ // Add or update new attributes
311
+ Object.keys(newProps).forEach(key => {
312
+ if (key === 'children' || key === 'text') return;
313
+ const newValue = newProps[key];
314
+ const oldValue = oldProps[key];
315
+
316
+ if (newValue !== oldValue) {
317
+ const attrName = key === 'className' ? 'class' : key;
318
+
319
+ if (newValue === true) {
320
+ domElement.setAttribute(attrName, '');
321
+ } else if (newValue === false || newValue === null) {
322
+ domElement.removeAttribute(attrName);
323
+ } else {
324
+ domElement.setAttribute(attrName, String(newValue));
325
+ }
326
+ }
327
+ });
328
+ },
329
+
330
+ // Patch children with intelligent list diffing
331
+ patchChildren(domElement, oldVNode, newVNode) {
332
+ // Check if we're in a browser environment
333
+ if (typeof window === 'undefined' || typeof Node === 'undefined' || typeof document === 'undefined') {
334
+ return;
335
+ }
336
+
337
+ // Check if domElement has required methods
338
+ if (!domElement || typeof domElement.childNodes === 'undefined' || typeof domElement.appendChild !== 'function') {
339
+ return;
340
+ }
341
+
342
+ const oldTagName = oldVNode ? Object.keys(oldVNode)[0] : null;
343
+ const newTagName = Object.keys(newVNode)[0];
344
+ const oldProps = oldVNode && oldTagName ? (oldVNode[oldTagName] || {}) : {};
345
+ const newProps = newVNode[newTagName] || {};
346
+
347
+ // Extract children arrays
348
+ let oldChildren = [];
349
+ let newChildren = [];
350
+
351
+ // Handle old children
352
+ if (oldProps.children) {
353
+ oldChildren = Array.isArray(oldProps.children) ? oldProps.children : [oldProps.children];
354
+ } else if (oldProps.text) {
355
+ oldChildren = [oldProps.text];
356
+ }
357
+
358
+ // Handle new children
359
+ if (newProps.children) {
360
+ newChildren = Array.isArray(newProps.children) ? newProps.children : [newProps.children];
361
+ } else if (newProps.text) {
362
+ newChildren = [newProps.text];
363
+ }
364
+
365
+ // Get current DOM children (excluding text nodes that are just whitespace)
366
+ // Check if Array.from is available and domElement.childNodes is iterable
367
+ let domChildren = [];
368
+ if (typeof Array.from === 'function' && domElement.childNodes) {
369
+ try {
370
+ domChildren = Array.from(domElement.childNodes).filter(node => {
371
+ return node.nodeType === Node.ELEMENT_NODE ||
372
+ (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim());
373
+ });
374
+ } catch (_error) {
375
+ // Fallback to empty array if Array.from fails
376
+ console.warn('Failed to convert childNodes to array:', _error);
377
+ domChildren = [];
378
+ }
379
+ }
380
+
381
+ // Simple diffing algorithm - can be improved with key-based diffing
382
+ const maxLength = Math.max(oldChildren.length, newChildren.length, domChildren.length);
383
+
384
+ for (let i = 0; i < maxLength; i++) {
385
+ const oldChild = oldChildren[i];
386
+ const newChild = newChildren[i];
387
+ const domChild = domChildren[i];
388
+
389
+ if (newChild === undefined) {
390
+ // Remove extra DOM children
391
+ if (domChild && typeof domChild.remove === 'function') {
392
+ domChild.remove();
393
+ }
394
+ } else if (domChild === undefined) {
395
+ // Add new DOM children
396
+ const newElement = this.createDOMElement(newChild);
397
+ if (newElement) {
398
+ domElement.appendChild(newElement);
399
+ }
400
+ } else {
401
+ // Patch existing child
402
+ this.patchDOM(domChild, oldChild, newChild);
403
+ }
404
+ }
405
+ },
406
+
407
+ // Create DOM element from virtual element
408
+ createDOMElement(vNode) {
409
+ if (typeof vNode === 'string' || typeof vNode === 'number') {
410
+ return document.createTextNode(String(vNode));
411
+ }
412
+
413
+ if (!vNode || typeof vNode !== 'object') {
414
+ return document.createTextNode('');
415
+ }
416
+
417
+ if (Array.isArray(vNode)) {
418
+ const fragment = document.createDocumentFragment();
419
+ vNode.forEach(child => {
420
+ fragment.appendChild(this.createDOMElement(child));
421
+ });
422
+ return fragment;
423
+ }
424
+
425
+ const tagName = Object.keys(vNode)[0];
426
+ const props = vNode[tagName] || {};
427
+ const element = document.createElement(tagName);
428
+
429
+ // Set attributes
430
+ Object.keys(props).forEach(key => {
431
+ if (key === 'children' || key === 'text') return;
432
+
433
+ const value = props[key];
434
+ const attrName = key === 'className' ? 'class' : key;
435
+
436
+ if (value === true) {
437
+ element.setAttribute(attrName, '');
438
+ } else if (value !== false && value !== null) {
439
+ element.setAttribute(attrName, String(value));
440
+ }
441
+ });
442
+
443
+ // Add children
444
+ if (props.children) {
445
+ const children = Array.isArray(props.children) ? props.children : [props.children];
446
+ children.forEach(child => {
447
+ element.appendChild(this.createDOMElement(child));
448
+ });
449
+ } else if (props.text) {
450
+ element.appendChild(document.createTextNode(String(props.text)));
451
+ }
452
+
453
+ return element;
454
+ },
455
+
456
+ // Render virtual element to HTML string
457
+ renderVirtualElement(element) {
458
+ if (typeof element === 'string' || typeof element === 'number') {
459
+ return String(element);
460
+ }
461
+
462
+ if (!element || typeof element !== 'object') {
463
+ return '';
464
+ }
465
+
466
+ // Handle arrays of elements
467
+ if (Array.isArray(element)) {
468
+ return element.map(el => this.renderVirtualElement(el)).join('');
469
+ }
470
+
471
+ // Handle Coherent.js object syntax
472
+ const tagName = Object.keys(element)[0];
473
+ const props = element[tagName];
474
+
475
+ if (!props || typeof props !== 'object') {
476
+ return `<${tagName}></${tagName}>`;
477
+ }
478
+
479
+ // Build attributes
480
+ let attributes = '';
481
+ const children = [];
482
+
483
+ Object.keys(props).forEach(key => {
484
+ if (key === 'children') {
485
+ if (Array.isArray(props.children)) {
486
+ children.push(...props.children);
487
+ } else {
488
+ children.push(props.children);
489
+ }
490
+ } else if (key === 'text') {
491
+ children.push(props.text);
492
+ } else {
493
+ const attrName = key === 'className' ? 'class' : key;
494
+ const value = props[key];
495
+ if (value === true) {
496
+ attributes += ` ${attrName}`;
497
+ } else if (value !== false && value !== null && value !== undefined) {
498
+ attributes += ` ${attrName}="${String(value).replace(/"/g, '&quot;')}"`;
499
+ }
500
+ }
501
+ });
502
+
503
+ // Render children
504
+ const childrenHTML = children.map(child => this.renderVirtualElement(child)).join('');
505
+
506
+ // Check if it's a void element
507
+ const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
508
+
509
+ if (voidElements.has(tagName.toLowerCase())) {
510
+ return `<${tagName}${attributes}>`;
511
+ }
512
+
513
+ return `<${tagName}${attributes}>${childrenHTML}</${tagName}>`;
514
+ },
515
+
516
+ // Destroy the component and clean up
517
+ destroy() {
518
+ // Remove event listeners
519
+ this.eventListeners.forEach(({element, event, handler}) => {
520
+ if (element.removeEventListener) {
521
+ element.removeEventListener(event, handler);
522
+ }
523
+ });
524
+
525
+ // Clean up state
526
+ this.state = null;
527
+ this.isHydrated = false;
528
+
529
+ // Remove from instances map
530
+ componentInstances.delete(this.element);
531
+
532
+ // Component destroyed
533
+ },
534
+
535
+ // Set state (for components with state)
536
+ setState(newState) {
537
+ if (!this.state) {
538
+ this.state = {};
539
+ }
540
+
541
+ const oldState = {...this.state};
542
+ this.state = typeof newState === 'function' ?
543
+ {...this.state, ...newState(this.state)} :
544
+ {...this.state, ...newState};
545
+
546
+ // Trigger re-render
547
+ this.rerender();
548
+
549
+ // Call state change callback if exists
550
+ if (this.onStateChange) {
551
+ this.onStateChange(this.state, oldState);
552
+ }
553
+ },
554
+
555
+ // Add event listener that will be cleaned up on destroy
556
+ addEventListener(targetElement, event, handler) {
557
+ if (targetElement.addEventListener) {
558
+ targetElement.addEventListener(event, handler);
559
+ this.eventListeners.push({element: targetElement, event, handler});
560
+ }
561
+ }
562
+ };
563
+
564
+ // Store instance
565
+ componentInstances.set(element, instance);
566
+
567
+ // Store the instance on the root element for event handler access
568
+ if (element && typeof element.setAttribute === 'function') {
569
+ element.__coherentInstance = instance;
570
+ // Also add a data attribute to identify this as a coherent component
571
+ if (!element.hasAttribute('data-coherent-component')) {
572
+ element.setAttribute('data-coherent-component', 'true');
573
+ }
574
+ }
575
+
576
+ // Re-execute component to get fresh virtual DOM with function handlers
577
+ // For withState components, we need to ensure the state management is properly initialized
578
+ const componentProps = { ...instance.props };
579
+
580
+ // If this is a withState component, initialize its state properly
581
+ if (instance.component.__stateContainer) {
582
+ // Initialize state container with hydrated state if available
583
+ if (instance.state) {
584
+ instance.component.__stateContainer.setState(instance.state);
585
+ }
586
+
587
+ // Override the instance setState to use the state container
588
+ instance.setState = (newState) => {
589
+ // Update the state container
590
+ instance.component.__stateContainer.setState(newState);
591
+
592
+ // Update the instance state for consistency
593
+ const updatedState = instance.component.__stateContainer.getState();
594
+ instance.state = updatedState;
595
+
596
+ // Trigger re-render
597
+ instance.rerender();
598
+ };
599
+ }
600
+
601
+ // Execute component function to get fresh virtual DOM
602
+ const freshVirtualElement = instance.component(componentProps);
603
+
604
+ // Try to inspect the structure more carefully
605
+ if (freshVirtualElement && typeof freshVirtualElement === 'object') {
606
+ const tagName = Object.keys(freshVirtualElement)[0];
607
+ // Process root element and children
608
+ if (freshVirtualElement[tagName]) {
609
+ }
610
+ }
611
+
612
+ // Attach function-based event listeners from the fresh virtual DOM
613
+ attachFunctionEventListeners(element, freshVirtualElement, instance);
614
+
615
+ // Skip legacy event listeners to avoid conflicts with function handlers
616
+ // Skip legacy event attachment to prevent conflicts
617
+
618
+ // Component hydrated
619
+
620
+ return instance;
621
+ }
622
+
623
+ // Global registry for event handlers
624
+ const eventRegistry = {};
625
+
626
+ /**
627
+ * Register an event handler for later use
628
+ * @param {string} id - Unique identifier for the event handler
629
+ * @param {Function} handler - The event handler function
630
+ */
631
+ export function registerEventHandler(id, handler) {
632
+ eventRegistry[id] = handler;
633
+ }
634
+
635
+ /**
636
+ * Global event handler that can be called from inline event attributes
637
+ * @param {string} eventId - The event handler ID
638
+ * @param {Element} element - The DOM element
639
+ * @param {Event} event - The event object
640
+ */
641
+ if (typeof window !== 'undefined') {
642
+ // Initialize event registries if they don't exist
643
+ window.__coherentEventRegistry = window.__coherentEventRegistry || {};
644
+ window.__coherentActionRegistry = window.__coherentActionRegistry || {};
645
+
646
+ window.__coherentEventHandler = function(eventId, element, event) {
647
+ // Event handler called
648
+
649
+ // Try to get the function from the event registry first
650
+ let handlerFunc = window.__coherentEventRegistry[eventId];
651
+
652
+ // If not found in event registry, try action registry
653
+ if (!handlerFunc && window.__coherentActionRegistry[eventId]) {
654
+ handlerFunc = window.__coherentActionRegistry[eventId];
655
+ }
656
+
657
+ if (handlerFunc) {
658
+ // Try to find the component instance associated with this element
659
+ let componentElement = element;
660
+ while (componentElement && !componentElement.hasAttribute('data-coherent-component')) {
661
+ componentElement = componentElement.parentElement;
662
+ }
663
+
664
+ if (componentElement && componentElement.__coherentInstance) {
665
+ // We found the component instance
666
+ const instance = componentElement.__coherentInstance;
667
+ const state = instance.state || {};
668
+ const setState = instance.setState ? instance.setState.bind(instance) : (() => {});
669
+
670
+ try {
671
+ // Call the handler function with the element as context and pass event, state, setState
672
+ handlerFunc.call(element, event, state, setState);
673
+ } catch (_error) {
674
+ console.warn(`Error executing coherent event handler:`, _error);
675
+ }
676
+ } else {
677
+ // Fallback: call the handler without component context
678
+ try {
679
+ handlerFunc.call(element, event);
680
+ } catch (_error) {
681
+ console.warn(`Error executing coherent event handler (no component context):`, _error);
682
+ }
683
+ }
684
+ } else {
685
+ console.warn(`Event handler not found for ID: ${eventId}`);
686
+ }
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Updates DOM elements to reflect state changes using direct DOM manipulation.
692
+ * This function serves as the main entry point for synchronizing component state
693
+ * with the visual representation in the DOM.
694
+ *
695
+ * @param {HTMLElement} rootElement - The root component element containing the UI to update
696
+ * @param {Object} state - The new state object containing updated component data
697
+ * @since 0.1.2
698
+ */
699
+ function updateDOMWithState(rootElement, state) {
700
+ if (!rootElement || !state) return;
701
+
702
+ // Use direct DOM updates to avoid breaking event handlers
703
+ updateDOMElementsDirectly(rootElement, state);
704
+
705
+ // Also update any dynamic content that needs to be re-rendered
706
+ updateDynamicContent(rootElement, state);
707
+ }
708
+
709
+ /**
710
+ * Simple virtual DOM to DOM rendering fallback
711
+ *
712
+ * @param {Object} vdom - Virtual DOM object
713
+ * @param {HTMLElement} container - Container element
714
+ */
715
+ // eslint-disable-next-line no-unused-vars -- kept for future SSR fallback rendering
716
+ function renderVirtualDOMToElement(vdom, container) {
717
+ if (!vdom || !container) return;
718
+
719
+ // Handle different virtual DOM structures
720
+ if (typeof vdom === 'string') {
721
+ container.textContent = vdom;
722
+ return;
723
+ }
724
+
725
+ if (typeof vdom === 'object') {
726
+ // Get the tag name (first key)
727
+ const tagName = Object.keys(vdom)[0];
728
+ if (!tagName) return;
729
+
730
+ const element = document.createElement(tagName);
731
+ const props = vdom[tagName] || {};
732
+
733
+ // Set attributes and properties
734
+ Object.keys(props).forEach(key => {
735
+ if (key === 'children') {
736
+ // Handle children
737
+ const children = props.children;
738
+ if (Array.isArray(children)) {
739
+ children.forEach(child => {
740
+ renderVirtualDOMToElement(child, element);
741
+ });
742
+ } else if (children) {
743
+ renderVirtualDOMToElement(children, element);
744
+ }
745
+ } else if (key === 'text') {
746
+ element.textContent = props[key];
747
+ } else if (key.startsWith('on')) {
748
+ // Skip event handlers for now - they'll be attached separately
749
+ } else {
750
+ // Set attribute
751
+ element.setAttribute(key, props[key]);
752
+ }
753
+ });
754
+
755
+ container.appendChild(element);
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Performs direct DOM updates by finding elements with data-ref attributes
761
+ * and updating their content to match the current state. This approach ensures
762
+ * that UI elements stay synchronized with component state without full re-rendering.
763
+ *
764
+ * @param {HTMLElement} rootElement - The root component element to search within
765
+ * @param {Object} state - The current state object containing updated values
766
+ * @since 0.1.2
767
+ */
768
+ function updateDOMElementsDirectly(rootElement, state) {
769
+ // Update elements with data-ref attributes that correspond to state values
770
+ const refElements = rootElement.querySelectorAll('[data-ref]');
771
+ refElements.forEach(element => {
772
+ const ref = element.getAttribute('data-ref');
773
+ if (ref && state.hasOwnProperty(ref)) {
774
+ // Update text content based on the reference
775
+ if (ref === 'count') {
776
+ element.textContent = `Count: ${state.count}`;
777
+ } else if (ref === 'step') {
778
+ element.textContent = `Step: ${state.step}`;
779
+ } else {
780
+ element.textContent = state[ref];
781
+ }
782
+ }
783
+ });
784
+
785
+ // Update input values that correspond to state
786
+ const inputs = rootElement.querySelectorAll('input');
787
+ inputs.forEach(input => {
788
+ if (input.type === 'number' && state.step !== undefined) {
789
+ input.value = state.step;
790
+ } else if (input.type === 'text' && state.newTodo !== undefined) {
791
+ // DON'T override input value if user is actively typing
792
+ // Only update if the input is not focused (user not typing)
793
+ if (document.activeElement !== input) {
794
+ input.value = state.newTodo;
795
+ }
796
+ }
797
+ });
798
+ }
799
+
800
+ /**
801
+ * Updates dynamic content sections such as lists, statistics, and interactive elements.
802
+ * This function handles complex UI updates that require more than simple text replacement,
803
+ * including filtering, sorting, and structural changes to the DOM.
804
+ *
805
+ * @param {HTMLElement} rootElement - The root component element containing dynamic content
806
+ * @param {Object} state - The current state object with updated data
807
+ * @since 0.1.2
808
+ */
809
+ function updateDynamicContent(rootElement, state) {
810
+ // Update todo list if present
811
+ if (state.todos !== undefined) {
812
+ updateTodoList(rootElement, state);
813
+ }
814
+
815
+ // Update todo stats if present
816
+ if (state.todos !== undefined) {
817
+ updateTodoStats(rootElement, state);
818
+ }
819
+
820
+ // Update filter buttons if present
821
+ if (state.filter !== undefined) {
822
+ updateFilterButtons(rootElement, state);
823
+ }
824
+ }
825
+
826
+ /**
827
+ * Updates the todo list display by rebuilding the list items based on current state.
828
+ * Handles filtering (all/active/completed) and creates new DOM elements for each todo.
829
+ * After updating the DOM, re-attaches event handlers to ensure interactivity.
830
+ *
831
+ * @param {HTMLElement} rootElement - The root component element containing the todo list
832
+ * @param {Object} state - The current state object containing todos array and filter settings
833
+ * @since 0.1.2
834
+ */
835
+ function updateTodoList(rootElement, state) {
836
+ const todoList = rootElement.querySelector('.todo-list');
837
+ if (!todoList) return;
838
+
839
+ // Filter todos based on current filter
840
+ const filteredTodos = state.todos.filter(todo => {
841
+ if (state.filter === 'active') return !todo.completed;
842
+ if (state.filter === 'completed') return todo.completed;
843
+ return true;
844
+ });
845
+
846
+ // Clear current list
847
+ todoList.innerHTML = '';
848
+
849
+ // Add filtered todos
850
+ filteredTodos.forEach(todo => {
851
+ const li = document.createElement('li');
852
+ li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
853
+
854
+ li.innerHTML = `
855
+ <input type="checkbox" ${todo.completed ? 'checked' : ''} class="todo-checkbox" data-todo-id="${todo.id}">
856
+ <span class="todo-text">${todo.text}</span>
857
+ <button class="btn btn-danger btn-small" data-todo-id="${todo.id}" data-action="remove">×</button>
858
+ `;
859
+
860
+ todoList.appendChild(li);
861
+ });
862
+
863
+ // Re-attach function-based event handlers to newly created DOM elements
864
+ // This is necessary because manually created DOM elements don't automatically get
865
+ // the function-based handlers from the virtual DOM
866
+ reattachTodoEventHandlers(rootElement, state);
867
+ }
868
+
869
+ /**
870
+ * Update todo statistics display
871
+ *
872
+ * @param {HTMLElement} rootElement - The root component element
873
+ * @param {Object} state - The new state
874
+ */
875
+ function updateTodoStats(rootElement, state) {
876
+ const statsElement = rootElement.querySelector('.todo-stats');
877
+ if (!statsElement || !state.todos) return;
878
+
879
+ const stats = {
880
+ total: state.todos.length,
881
+ completed: state.todos.filter(todo => todo.completed).length,
882
+ active: state.todos.filter(todo => !todo.completed).length
883
+ };
884
+
885
+ statsElement.innerHTML = `
886
+ <span class="stat-item">Total: ${stats.total}</span>
887
+ <span class="stat-item">Active: ${stats.active}</span>
888
+ <span class="stat-item">Completed: ${stats.completed}</span>
889
+ `;
890
+ }
891
+
892
+ /**
893
+ * Update filter button states
894
+ *
895
+ * @param {HTMLElement} rootElement - The root component element
896
+ * @param {Object} state - The new state
897
+ */
898
+ function updateFilterButtons(rootElement, state) {
899
+ const filterButtons = rootElement.querySelectorAll('.filter-btn');
900
+ filterButtons.forEach(button => {
901
+ const buttonText = button.textContent.toLowerCase();
902
+ if (buttonText === state.filter || (buttonText === 'all' && state.filter === 'all')) {
903
+ button.classList.add('active');
904
+ } else {
905
+ button.classList.remove('active');
906
+ }
907
+ });
908
+ }
909
+
910
+ /**
911
+ * Re-attaches event handlers to dynamically created todo items after DOM updates.
912
+ * This function is essential for maintaining interactivity when todo items are
913
+ * recreated during state changes. Handles both delete buttons and toggle checkboxes.
914
+ *
915
+ * @param {HTMLElement} rootElement - The root component element containing todo items
916
+ * @param {Object} state - The current state object for context
917
+ * @since 0.1.2
918
+ */
919
+ function reattachTodoEventHandlers(rootElement) {
920
+ // Find the component instance to get access to the component's handlers
921
+ const componentInstance = rootElement.__coherentInstance;
922
+ if (!componentInstance || !componentInstance.component) {
923
+ console.warn('⚠️ No component instance found for re-attaching todo event handlers');
924
+ return;
925
+ }
926
+
927
+ // Get the component's removeTodo and toggleTodo functions
928
+ // These should be available in the component's scope
929
+ const component = componentInstance.component;
930
+
931
+ // Re-attach delete button handlers
932
+ const deleteButtons = rootElement.querySelectorAll('button[data-action="remove"]');
933
+ deleteButtons.forEach(button => {
934
+ const todoId = parseInt(button.getAttribute('data-todo-id'));
935
+ if (todoId) {
936
+ // Remove any existing handler to prevent duplicates
937
+ const handlerKey = `__coherent_click_handler`;
938
+ if (button[handlerKey]) {
939
+ button.removeEventListener('click', button[handlerKey]);
940
+ }
941
+
942
+ // Create new handler that calls the component's removeTodo function
943
+ const clickHandler = (event) => {
944
+ event.preventDefault();
945
+
946
+ // Get current state and setState from component
947
+ if (component.__stateContainer) {
948
+ const currentState = component.__stateContainer.getState();
949
+ const setState = component.__stateContainer.setState.bind(component.__stateContainer);
950
+
951
+ // Remove the todo
952
+ setState({
953
+ todos: currentState.todos.filter(todo => todo.id !== todoId)
954
+ });
955
+
956
+ // Trigger DOM update to reflect the state change
957
+ const updatedState = component.__stateContainer.getState();
958
+ updateDOMWithState(rootElement, updatedState);
959
+ }
960
+ };
961
+
962
+ // Attach the handler
963
+ button.addEventListener('click', clickHandler);
964
+ button[handlerKey] = clickHandler;
965
+ }
966
+ });
967
+
968
+ // Re-attach checkbox handlers
969
+ const checkboxes = rootElement.querySelectorAll('.todo-checkbox');
970
+ checkboxes.forEach(checkbox => {
971
+ const todoId = parseInt(checkbox.getAttribute('data-todo-id'));
972
+ if (todoId) {
973
+ // Remove any existing handler to prevent duplicates
974
+ const handlerKey = `__coherent_change_handler`;
975
+ if (checkbox[handlerKey]) {
976
+ checkbox.removeEventListener('change', checkbox[handlerKey]);
977
+ }
978
+
979
+ // Create new handler that calls the component's toggleTodo function
980
+ const changeHandler = () => {
981
+ // Get current state and setState from component
982
+ if (component.__stateContainer) {
983
+ const currentState = component.__stateContainer.getState();
984
+ const setState = component.__stateContainer.setState.bind(component.__stateContainer);
985
+
986
+ // Toggle the todo
987
+ setState({
988
+ todos: currentState.todos.map(todo =>
989
+ todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
990
+ )
991
+ });
992
+
993
+ // Trigger DOM update to reflect the state change
994
+ const updatedState = component.__stateContainer.getState();
995
+ updateDOMWithState(rootElement, updatedState);
996
+ }
997
+ };
998
+
999
+ // Attach the handler
1000
+ checkbox.addEventListener('change', changeHandler);
1001
+ checkbox[handlerKey] = changeHandler;
1002
+ }
1003
+ });
1004
+ }
1005
+
1006
+ /**
1007
+ * Attaches function-based event listeners from virtual DOM definitions to real DOM elements.
1008
+ * This is the core mechanism that enables interactive components by bridging the gap
1009
+ * between virtual DOM event handlers and actual browser events. Prevents duplicate
1010
+ * handlers and ensures proper state management integration.
1011
+ *
1012
+ * @param {HTMLElement} rootElement - The root DOM element to search for event targets
1013
+ * @param {Object} virtualElement - The virtual DOM element containing function handlers
1014
+ * @param {Object} instance - The component instance providing state and context
1015
+ * @since 0.1.2
1016
+ */
1017
+ function attachFunctionEventListeners(rootElement, virtualElement, instance, options = {}) {
1018
+ if (!rootElement || !virtualElement || typeof window === 'undefined') {
1019
+ return;
1020
+ }
1021
+
1022
+ // Helper function to traverse virtual DOM and find function handlers
1023
+ function traverseAndAttach(domElement, vElement, path = []) {
1024
+ if (!vElement || typeof vElement !== 'object') return;
1025
+
1026
+ // Handle array of virtual elements
1027
+ if (Array.isArray(vElement)) {
1028
+ vElement.forEach((child, index) => {
1029
+ const childElement = domElement.children[index];
1030
+ if (childElement) {
1031
+ traverseAndAttach(childElement, child, [...path, index]);
1032
+ }
1033
+ });
1034
+ return;
1035
+ }
1036
+
1037
+ // Handle single virtual element
1038
+ const tagName = Object.keys(vElement)[0];
1039
+ const elementProps = vElement[tagName];
1040
+
1041
+ if (elementProps && typeof elementProps === 'object') {
1042
+ // Look for event handler functions
1043
+ const eventHandlers = ['onclick', 'onchange', 'oninput', 'onfocus', 'onblur', 'onsubmit', 'onkeypress', 'onkeydown', 'onkeyup', 'onmouseenter', 'onmouseleave'];
1044
+
1045
+ eventHandlers.forEach(eventName => {
1046
+ const handler = elementProps[eventName];
1047
+ if (typeof handler === 'function') {
1048
+ const eventType = eventName.substring(2); // Remove 'on' prefix
1049
+
1050
+ // If inputsOnly option is set, only attach input-related events and click events on dynamically generated elements
1051
+ if (options.inputsOnly) {
1052
+ const inputEvents = ['input', 'change', 'keypress'];
1053
+ const isDynamicElement = domElement.closest('.todo-item') || domElement.closest('[data-dynamic]');
1054
+
1055
+ if (!inputEvents.includes(eventType) && !(eventType === 'click' && isDynamicElement)) {
1056
+ return; // Skip non-input events except clicks on dynamic elements
1057
+ }
1058
+ }
1059
+
1060
+ // Special handling for input events
1061
+
1062
+ // Check if handler is already attached to prevent duplicates
1063
+ const handlerKey = `__coherent_${eventType}_handler`;
1064
+ if (domElement[handlerKey]) {
1065
+ // Remove the old handler first
1066
+ domElement.removeEventListener(eventType, domElement[handlerKey]);
1067
+ delete domElement[handlerKey];
1068
+ }
1069
+
1070
+ // Create a wrapper that provides component context
1071
+ const wrappedHandler = (event) => {
1072
+ try {
1073
+ // Only prevent default for non-input events and non-form events
1074
+ if (eventType !== 'input' && eventType !== 'change' && eventType !== 'keypress') {
1075
+ event.preventDefault();
1076
+ }
1077
+
1078
+ // Execute the function handler with proper context
1079
+
1080
+ // Extract state and setState from the component's current execution context
1081
+ let currentState = {};
1082
+ let currentSetState = () => {};
1083
+
1084
+ // For withState components, use the state container
1085
+ if (instance.component && instance.component.__stateContainer) {
1086
+ currentState = instance.component.__stateContainer.getState();
1087
+ currentSetState = (newState) => {
1088
+ // Call the component's setState method
1089
+ instance.component.__stateContainer.setState(newState);
1090
+
1091
+ // Update the instance state for consistency
1092
+ if (instance.state && typeof newState === 'object') {
1093
+ Object.assign(instance.state, newState);
1094
+ }
1095
+
1096
+ // Get the updated state after setState
1097
+ instance.component.__stateContainer.getState();
1098
+
1099
+ // Trigger component re-render to reflect the new state
1100
+ const componentRoot = domElement.closest('[data-coherent-component]');
1101
+ if (componentRoot && componentRoot.__coherentInstance) {
1102
+ componentRoot.__coherentInstance.rerender();
1103
+ }
1104
+ };
1105
+ } else if (instance.state) {
1106
+ // Fallback for non-withState components
1107
+ currentState = instance.state;
1108
+ currentSetState = (newState) => {
1109
+ if (typeof newState === 'object') {
1110
+ Object.assign(instance.state, newState);
1111
+ }
1112
+
1113
+ // Trigger component re-render to reflect the new state
1114
+ const componentRoot = domElement.closest('[data-coherent-component]');
1115
+ if (componentRoot && componentRoot.__coherentInstance) {
1116
+ componentRoot.__coherentInstance.rerender();
1117
+ }
1118
+ };
1119
+ }
1120
+
1121
+ // Call the original handler with event, state, and setState
1122
+ const result = handler.call(domElement, event, currentState, currentSetState);
1123
+
1124
+ return result;
1125
+ } catch (_error) {
1126
+ console.error(`Error in ${eventName} handler:`, _error);
1127
+ }
1128
+ };
1129
+
1130
+ // Remove any existing onclick attributes that might interfere
1131
+ if (domElement.hasAttribute(eventName)) {
1132
+ domElement.removeAttribute(eventName);
1133
+ }
1134
+
1135
+ // Store the handler reference to prevent duplicates
1136
+ domElement[handlerKey] = wrappedHandler;
1137
+
1138
+ // Add the new event listener (use capture only for non-input events)
1139
+ const useCapture = eventType !== 'input' && eventType !== 'change';
1140
+ domElement.addEventListener(eventType, wrappedHandler, useCapture);
1141
+
1142
+ // Input event handler attached successfully
1143
+
1144
+ // Add to instance's event listeners for cleanup
1145
+ if (instance.eventListeners && Array.isArray(instance.eventListeners)) {
1146
+ instance.eventListeners.push({
1147
+ element: domElement,
1148
+ event: eventType,
1149
+ handler: wrappedHandler
1150
+ });
1151
+ }
1152
+ }
1153
+ });
1154
+
1155
+ // Recursively handle children
1156
+ if (elementProps.children) {
1157
+ const children = Array.isArray(elementProps.children) ? elementProps.children : [elementProps.children];
1158
+ children.forEach((child, index) => {
1159
+ const childElement = domElement.children[index];
1160
+ if (childElement && child) {
1161
+ traverseAndAttach(childElement, child, [...path, 'children', index]);
1162
+ }
1163
+ });
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ // Start traversal from the root
1169
+ traverseAndAttach(rootElement, virtualElement);
1170
+ }
1171
+
1172
+ /**
1173
+ * Attach event listeners from data attributes
1174
+ *
1175
+ * @param {HTMLElement} element - The root element
1176
+ * @param {Object} instance - The component instance
1177
+ */
1178
+ function attachEventListeners(element, instance) {
1179
+ // Check if we're in a browser environment
1180
+ try {
1181
+ // Clear any existing event listeners if this is a re-hydration
1182
+ if (instance && instance.eventListeners && Array.isArray(instance.eventListeners)) {
1183
+ instance.eventListeners.forEach(({ element, event, handler }) => {
1184
+ if (element && typeof element.removeEventListener === 'function') {
1185
+ element.removeEventListener(event, handler);
1186
+ }
1187
+ });
1188
+ instance.eventListeners = [];
1189
+ }
1190
+
1191
+ // Check if we're in a browser environment
1192
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
1193
+ return;
1194
+ }
1195
+
1196
+ // Check if element has required methods
1197
+ if (!element || typeof element.querySelectorAll !== 'function') {
1198
+ return;
1199
+ }
1200
+
1201
+ // Find all elements with data-action attributes
1202
+ const actionElements = element.querySelectorAll('[data-action]');
1203
+
1204
+ actionElements.forEach(actionElement => {
1205
+ // Check if element has required methods
1206
+ if (!actionElement || typeof actionElement.getAttribute !== 'function') return;
1207
+
1208
+ const action = actionElement.getAttribute('data-action');
1209
+ const target = actionElement.getAttribute('data-target') || 'default';
1210
+ const event = actionElement.getAttribute('data-event') || 'click';
1211
+
1212
+ if (action) {
1213
+ const handler = (e) => {
1214
+ if (e && typeof e.preventDefault === 'function') {
1215
+ e.preventDefault(); // Prevent default behavior for better control
1216
+ }
1217
+ handleComponentAction(e, action, target, instance);
1218
+ };
1219
+
1220
+ // Add event listener
1221
+ if (typeof actionElement.addEventListener === 'function') {
1222
+ actionElement.addEventListener(event, handler);
1223
+
1224
+ // Store for cleanup
1225
+ if (instance.eventListeners && Array.isArray(instance.eventListeners)) {
1226
+ instance.eventListeners.push({
1227
+ element: actionElement,
1228
+ event,
1229
+ handler
1230
+ });
1231
+ }
1232
+ }
1233
+ }
1234
+ });
1235
+
1236
+ // Check for inline event attributes and warn users to use safer alternatives
1237
+ const eventAttributes = ['onclick', 'onchange', 'oninput', 'onfocus', 'onblur', 'onsubmit'];
1238
+
1239
+ eventAttributes.forEach(eventName => {
1240
+ const attributeSelector = `[${eventName}]`;
1241
+ const elements = element.querySelectorAll(attributeSelector);
1242
+
1243
+ elements.forEach(elementWithEvent => {
1244
+ // Check if element has required methods
1245
+ if (!elementWithEvent || typeof elementWithEvent.getAttribute !== 'function') return;
1246
+
1247
+ const eventAttr = elementWithEvent.getAttribute(eventName);
1248
+
1249
+ if (eventAttr) {
1250
+ // Warn about inline event attributes - they are not supported for security reasons
1251
+ console.warn(
1252
+ `[Coherent.js] Inline event attribute "${eventName}="${eventAttr}" found but not supported.\n` +
1253
+ `For security and CSP compliance, use one of these alternatives:\n` +
1254
+ `1. Function-based handlers: Pass functions directly in virtual DOM\n` +
1255
+ `2. Data-action registry: <button data-action="actionId" data-event="click">\n` +
1256
+ `3. Event registry: <button data-coherent-event="handlerId" data-coherent-event-type="click">\n` +
1257
+ `See documentation for details.`
1258
+ );
1259
+ }
1260
+ });
1261
+ });
1262
+
1263
+ // Also look for data-action attributes (new approach)
1264
+ const dataActionElements = element.querySelectorAll('[data-action]');
1265
+
1266
+ dataActionElements.forEach(actionElement => {
1267
+ // Check if element has required methods
1268
+ if (!actionElement || typeof actionElement.getAttribute !== 'function') return;
1269
+
1270
+ const actionId = actionElement.getAttribute('data-action');
1271
+ const eventType = actionElement.getAttribute('data-event') || 'click';
1272
+
1273
+ if (actionId) {
1274
+ // Get the function from the action registry
1275
+ let handlerFunc = null;
1276
+
1277
+ // Try to get from action registry (server-side stored)
1278
+ if (typeof window !== 'undefined' && window.__coherentActionRegistry && window.__coherentActionRegistry[actionId]) {
1279
+ handlerFunc = window.__coherentActionRegistry[actionId];
1280
+ } else {
1281
+ console.warn(`No handler found for action ${actionId}`, window.__coherentActionRegistry);
1282
+ }
1283
+
1284
+ if (handlerFunc && typeof handlerFunc === 'function') {
1285
+ // Mark as processed to avoid duplicate handling
1286
+ if (typeof actionElement.hasAttribute === 'function' && !actionElement.hasAttribute(`data-hydrated-${eventType}`)) {
1287
+ actionElement.setAttribute(`data-hydrated-${eventType}`, 'true');
1288
+
1289
+ const handler = (e) => {
1290
+ try {
1291
+ // Try to find the component instance associated with this element
1292
+ let componentElement = actionElement;
1293
+ while (componentElement && !componentElement.hasAttribute('data-coherent-component')) {
1294
+ componentElement = componentElement.parentElement;
1295
+ }
1296
+
1297
+ if (componentElement && componentElement.__coherentInstance) {
1298
+ // We found the component instance
1299
+ const instance = componentElement.__coherentInstance;
1300
+ const state = instance.state || {};
1301
+ const setState = instance.setState || (() => {});
1302
+
1303
+ // Call the handler function with the element as context and pass event, state, setState
1304
+ handlerFunc.call(actionElement, e, state, setState);
1305
+ } else {
1306
+ // Fallback: call the handler without component context
1307
+ handlerFunc.call(actionElement, e);
1308
+ }
1309
+ } catch (_error) {
1310
+ console.warn(`Error executing action handler for ${actionId}:`, _error);
1311
+ }
1312
+ };
1313
+
1314
+ if (typeof actionElement.addEventListener === 'function') {
1315
+ actionElement.addEventListener(eventType, handler);
1316
+ if (instance && instance.eventListeners && Array.isArray(instance.eventListeners)) {
1317
+ instance.eventListeners.push({
1318
+ element: actionElement,
1319
+ event: eventType,
1320
+ handler
1321
+ });
1322
+ }
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+ });
1328
+
1329
+ // Also look for Coherent-specific event handlers (data-coherent-event)
1330
+ const coherentEventElements = element.querySelectorAll('[data-coherent-event]');
1331
+
1332
+ coherentEventElements.forEach(elementWithCoherentEvent => {
1333
+ // Check if element has required methods
1334
+ if (!elementWithCoherentEvent || typeof elementWithCoherentEvent.getAttribute !== 'function') return;
1335
+
1336
+ const eventId = elementWithCoherentEvent.getAttribute('data-coherent-event');
1337
+ const eventType = elementWithCoherentEvent.getAttribute('data-coherent-event-type');
1338
+
1339
+ if (eventId && eventType) {
1340
+ // Get the function from the registry
1341
+ let handlerFunc = null;
1342
+
1343
+ // Try to get from global registry (server-side stored)
1344
+ if (typeof window !== 'undefined' && window.__coherentEventRegistry && window.__coherentEventRegistry[eventId]) {
1345
+ handlerFunc = window.__coherentEventRegistry[eventId];
1346
+ }
1347
+
1348
+ if (handlerFunc && typeof handlerFunc === 'function') {
1349
+ // Mark as processed to avoid duplicate handling
1350
+ if (typeof elementWithCoherentEvent.hasAttribute === 'function' && !elementWithCoherentEvent.hasAttribute(`data-hydrated-${eventType}`)) {
1351
+ elementWithCoherentEvent.setAttribute(`data-hydrated-${eventType}`, 'true');
1352
+
1353
+ const handler = (e) => {
1354
+ try {
1355
+ // Call the original function with proper context
1356
+ // Pass the event, state, and setState as parameters
1357
+ const state = instance.state || {};
1358
+ const setState = instance.setState || (() => {});
1359
+
1360
+ // Bind the function to the element and call it with the event
1361
+ handlerFunc.call(elementWithCoherentEvent, e, state, setState);
1362
+ } catch (_error) {
1363
+ console.warn(`Error executing coherent event handler:`, _error);
1364
+ }
1365
+ };
1366
+
1367
+ if (typeof elementWithCoherentEvent.addEventListener === 'function') {
1368
+ elementWithCoherentEvent.addEventListener(eventType, handler);
1369
+ if (instance.eventListeners && Array.isArray(instance.eventListeners)) {
1370
+ instance.eventListeners.push({
1371
+ element: elementWithCoherentEvent,
1372
+ event: eventType,
1373
+ handler
1374
+ });
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ });
1381
+
1382
+ } catch (_error) {
1383
+ console.warn('Error attaching event listeners:', _error);
1384
+ }
1385
+ }
1386
+
1387
+ /**
1388
+ * Handle component actions
1389
+ *
1390
+ * @param {Event} event - The DOM event
1391
+ * @param {string} action - The action name
1392
+ * @param {string} target - The target identifier
1393
+ * @param {Object} instance - The component instance
1394
+ */
1395
+ function handleComponentAction(event, action, target, instance) {
1396
+ // Handle common actions
1397
+ switch (action) {
1398
+ case 'increment':
1399
+ if (instance.state && instance.state.count !== undefined) {
1400
+ const step = instance.state.step || 1;
1401
+ instance.setState({count: instance.state.count + step});
1402
+
1403
+ // Update the DOM directly for immediate feedback
1404
+ const countElement = instance.element.querySelector('[data-ref="count"]');
1405
+ if (countElement) {
1406
+ countElement.textContent = `Count: ${instance.state.count + step}`;
1407
+ }
1408
+ }
1409
+ break;
1410
+ case 'decrement':
1411
+ if (instance.state && instance.state.count !== undefined) {
1412
+ const step = instance.state.step || 1;
1413
+ instance.setState({count: instance.state.count - step});
1414
+
1415
+ // Update the DOM directly for immediate feedback
1416
+ const countElement = instance.element.querySelector('[data-ref="count"]');
1417
+ if (countElement) {
1418
+ countElement.textContent = `Count: ${instance.state.count - step}`;
1419
+ }
1420
+ }
1421
+ break;
1422
+ case 'reset':
1423
+ if (instance.state) {
1424
+ const initialCount = instance.props.initialCount || 0;
1425
+ instance.setState({count: initialCount});
1426
+
1427
+ // Update the DOM directly for immediate feedback
1428
+ const countElement = instance.element.querySelector('[data-ref="count"]');
1429
+ if (countElement) {
1430
+ countElement.textContent = `Count: ${initialCount}`;
1431
+ }
1432
+ }
1433
+ break;
1434
+ case 'changeStep':
1435
+ // Get the input value
1436
+ const inputElement = event.target;
1437
+ if (inputElement && inputElement.value) {
1438
+ const stepValue = parseInt(inputElement.value, 10);
1439
+ if (!isNaN(stepValue) && stepValue >= 1 && stepValue <= 10) {
1440
+ instance.setState({step: stepValue});
1441
+
1442
+ // Update the DOM directly for immediate feedback
1443
+ const stepElement = instance.element.querySelector('[data-ref="step"]');
1444
+ if (stepElement) {
1445
+ stepElement.textContent = `Step: ${stepValue}`;
1446
+ }
1447
+ }
1448
+ }
1449
+ break;
1450
+ case 'toggle':
1451
+ const todoIndex = event.target && event.target.getAttribute ?
1452
+ parseInt(event.target.getAttribute('data-todo-index')) : -1;
1453
+ if (todoIndex >= 0 && instance.state && instance.state.todos && instance.state.todos[todoIndex]) {
1454
+ const newTodos = [...instance.state.todos];
1455
+ newTodos[todoIndex].completed = !newTodos[todoIndex].completed;
1456
+ instance.setState({todos: newTodos});
1457
+ }
1458
+ break;
1459
+ case 'add':
1460
+ if (typeof document !== 'undefined' && document.getElementById) {
1461
+ const input = document.getElementById(`new-todo-${target}`);
1462
+ if (input && input.value && input.value.trim()) {
1463
+ if (instance.state && instance.state.todos) {
1464
+ const newTodos = [
1465
+ ...instance.state.todos,
1466
+ { text: input.value.trim(), completed: false }
1467
+ ];
1468
+ instance.setState({todos: newTodos});
1469
+ input.value = '';
1470
+ }
1471
+ }
1472
+ }
1473
+ break;
1474
+ default:
1475
+ // Check if this is a custom method on the instance
1476
+ if (instance && typeof instance[action] === 'function') {
1477
+ try {
1478
+ // Call the custom method on the instance
1479
+ instance[action](event, target);
1480
+ } catch (_error) {
1481
+ console.warn(`Error executing custom action ${action}:`, _error);
1482
+ }
1483
+ } else {
1484
+ // Check if this is a function handler in the action registry
1485
+ if (typeof window !== 'undefined' && window.__coherentActionRegistry && window.__coherentActionRegistry[action]) {
1486
+ const handlerFunc = window.__coherentActionRegistry[action];
1487
+
1488
+ // Get the component state and setState function if available
1489
+ const state = instance ? (instance.state || {}) : {};
1490
+ const setState = instance && instance.setState ? instance.setState.bind(instance) : (() => {});
1491
+
1492
+ try {
1493
+ // Call the handler function with event, state, and setState
1494
+ handlerFunc(event, state, setState);
1495
+ } catch (_error) {
1496
+ console.warn(`Error executing action handler ${action}:`, _error);
1497
+ }
1498
+ } else {
1499
+ // Custom action handling would go here
1500
+ // Custom action executed
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+
1506
+ /**
1507
+ * Hydrate multiple elements with their corresponding components
1508
+ *
1509
+ * @param {Array} elements - Array of DOM elements to hydrate
1510
+ * @param {Array} components - Array of Coherent component functions
1511
+ * @param {Array} propsArray - Array of props for each component
1512
+ * @returns {Array} Array of hydrated component instances
1513
+ */
1514
+ function hydrateAll(elements, components, propsArray = []) {
1515
+ if (elements.length !== components.length) {
1516
+ throw new Error('Number of elements must match number of components');
1517
+ }
1518
+
1519
+ return elements.map((element, index) => {
1520
+ const component = components[index];
1521
+ const props = propsArray[index] || {};
1522
+ return hydrate(element, component, props);
1523
+ });
1524
+ }
1525
+
1526
+ /**
1527
+ * Find and hydrate elements by CSS selector
1528
+ *
1529
+ * @param {string} selector - CSS selector to find elements
1530
+ * @param {Function} component - The Coherent component function
1531
+ * @param {Object} props - The props to pass to the component
1532
+ * @returns {Array} Array of hydrated component instances
1533
+ */
1534
+ function hydrateBySelector(selector, component, props = {}) {
1535
+ if (typeof window === 'undefined' || !document.querySelectorAll) {
1536
+ return [];
1537
+ }
1538
+
1539
+ const elements = document.querySelectorAll(selector);
1540
+ return Array.from(elements).map(element => hydrate(element, component, props));
1541
+ }
1542
+
1543
+ /**
1544
+ * Enable client-side interactivity for event handlers
1545
+ *
1546
+ * @param {HTMLElement} rootElement - The root element to enable events on
1547
+ */
1548
+ function enableClientEvents(rootElement = document) {
1549
+ if (typeof window === 'undefined' || !rootElement.querySelectorAll) {
1550
+ return;
1551
+ }
1552
+
1553
+ // This function is now handled automatically during hydration
1554
+ // but can be called to enable events on dynamically added elements
1555
+ // Client events enabled
1556
+ }
1557
+
1558
+ /**
1559
+ * Create a hydratable component
1560
+ *
1561
+ * @param {Function} component - The Coherent component function
1562
+ * @param {Object} options - Hydration options
1563
+ * @returns {Function} A component that can be hydrated
1564
+ */
1565
+ function makeHydratable(component, options = {}) {
1566
+ // Extract component name from options or use function name
1567
+ const componentName = options.componentName || component.name || 'AnonymousComponent';
1568
+
1569
+ // Create a new function that wraps the original component
1570
+ const hydratableComponent = function(props = {}) {
1571
+ return component(props);
1572
+ };
1573
+
1574
+ // Set the component name on the hydratable component function
1575
+ Object.defineProperty(hydratableComponent, 'name', {
1576
+ value: componentName,
1577
+ writable: false
1578
+ });
1579
+
1580
+ // Copy all properties from the original component, including withState metadata
1581
+ Object.keys(component).forEach(key => {
1582
+ hydratableComponent[key] = component[key];
1583
+ });
1584
+
1585
+ // Copy prototype if it exists
1586
+ if (component.prototype) {
1587
+ hydratableComponent.prototype = Object.create(component.prototype);
1588
+ }
1589
+
1590
+ // Special handling for withState wrapped components
1591
+ if (component.__wrappedComponent && component.__stateContainer) {
1592
+ hydratableComponent.__wrappedComponent = component.__wrappedComponent;
1593
+ hydratableComponent.__stateContainer = component.__stateContainer;
1594
+ }
1595
+
1596
+ // Add hydration metadata to the component
1597
+ hydratableComponent.isHydratable = true;
1598
+ hydratableComponent.hydrationOptions = options;
1599
+
1600
+ // Add auto-hydration functionality
1601
+ hydratableComponent.autoHydrate = function(componentRegistry = {}) {
1602
+ // Register this component if not already registered
1603
+ if (!componentRegistry[hydratableComponent.name || 'AnonymousComponent']) {
1604
+ componentRegistry[hydratableComponent.name || 'AnonymousComponent'] = hydratableComponent;
1605
+ }
1606
+
1607
+ // Call the global autoHydrate function
1608
+ autoHydrate(componentRegistry);
1609
+ };
1610
+
1611
+ // Mark this component as hydratable
1612
+ hydratableComponent.isHydratable = true;
1613
+
1614
+ // Add a method to manually set hydration data for cases where we need to override
1615
+ hydratableComponent.withHydrationData = function(customProps = {}, customState = null) {
1616
+ return {
1617
+ render: function(props = {}) {
1618
+ const mergedProps = { ...customProps, ...props };
1619
+ const result = hydratableComponent(mergedProps);
1620
+ const hydrationData = hydratableComponent.getHydrationData(mergedProps, customState);
1621
+
1622
+ // Add hydration attributes to the root element
1623
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
1624
+ const tagName = Object.keys(result)[0];
1625
+ const elementProps = result[tagName];
1626
+
1627
+ if (elementProps && typeof elementProps === 'object') {
1628
+ // Add hydration attributes
1629
+ Object.keys(hydrationData.hydrationAttributes).forEach(attr => {
1630
+ const value = hydrationData.hydrationAttributes[attr];
1631
+ if (value !== null) {
1632
+ elementProps[attr] = value;
1633
+ }
1634
+ });
1635
+ }
1636
+ }
1637
+
1638
+ return result;
1639
+ }
1640
+ };
1641
+ };
1642
+
1643
+ // Add a method to get hydration data
1644
+ hydratableComponent.getHydrationData = function(props = {}, state = null) {
1645
+ return {
1646
+ componentName: componentName,
1647
+ props,
1648
+ initialState: options.initialState,
1649
+ // Add data attributes for hydration
1650
+ hydrationAttributes: {
1651
+ 'data-coherent-component': componentName,
1652
+ 'data-coherent-state': state ? JSON.stringify(state) : (options.initialState ? JSON.stringify(options.initialState) : null),
1653
+ 'data-coherent-props': Object.keys(props).length > 0 ? JSON.stringify(props) : null
1654
+ }
1655
+ };
1656
+ };
1657
+
1658
+ // Add a method to render with hydration data
1659
+ hydratableComponent.renderWithHydration = function(props = {}) {
1660
+ const result = hydratableComponent(props);
1661
+
1662
+ // Try to extract state from the component if it's a withState wrapped component
1663
+ let state = null;
1664
+ if (hydratableComponent.__wrappedComponent && hydratableComponent.__stateContainer) {
1665
+ // This is a withState wrapped component, try to get its state
1666
+ try {
1667
+ state = hydratableComponent.__stateContainer.getState();
1668
+ } catch (e) {
1669
+ // If we can't get the state, that's OK
1670
+ console.warn('Could not get component state:', e);
1671
+ }
1672
+ }
1673
+
1674
+ const hydrationData = hydratableComponent.getHydrationData(props, state);
1675
+
1676
+ // Add hydration attributes to the root element
1677
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
1678
+ const tagName = Object.keys(result)[0];
1679
+ const elementProps = result[tagName];
1680
+
1681
+ if (elementProps && typeof elementProps === 'object') {
1682
+ // Add hydration attributes
1683
+ Object.keys(hydrationData.hydrationAttributes).forEach(attr => {
1684
+ const value = hydrationData.hydrationAttributes[attr];
1685
+ if (value !== null) {
1686
+ elementProps[attr] = value;
1687
+ }
1688
+ });
1689
+ }
1690
+ }
1691
+
1692
+ return result;
1693
+ };
1694
+
1695
+ return hydratableComponent;
1696
+ }
1697
+
1698
+ /**
1699
+ * Auto-hydrate all components on page load
1700
+ *
1701
+ * @param {Object} componentRegistry - Registry of component functions
1702
+ */
1703
+ function autoHydrate(componentRegistry = {}) {
1704
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
1705
+ return;
1706
+ }
1707
+
1708
+ // autoHydrate called
1709
+
1710
+ // Check if registry is actually the window object (common mistake)
1711
+ if (componentRegistry === window) {
1712
+ console.warn('⚠️ Component registry is the window object! This suggests the registry was not properly initialized.');
1713
+ // Falling back to window.componentRegistry
1714
+ componentRegistry = window.componentRegistry || {};
1715
+ }
1716
+
1717
+ // Initialize registries if they don't exist
1718
+ window.__coherentEventRegistry = window.__coherentEventRegistry || {};
1719
+ window.__coherentActionRegistry = window.__coherentActionRegistry || {};
1720
+
1721
+ // Wait for DOM to be ready
1722
+ const hydrateComponents = () => {
1723
+ const hydrateableElements = document.querySelectorAll('[data-coherent-component]');
1724
+
1725
+ hydrateableElements.forEach(element => {
1726
+ const componentName = element.getAttribute('data-coherent-component');
1727
+
1728
+ // Look for the component in the registry
1729
+ let component = componentRegistry[componentName];
1730
+
1731
+ // If not found by exact name, try to find it by checking if it's a hydratable component
1732
+ if (!component) {
1733
+ // Component not found by name, searching registry...
1734
+ for (const comp of Object.values(componentRegistry)) {
1735
+ if (comp && comp.isHydratable) {
1736
+ component = comp;
1737
+ break;
1738
+ }
1739
+ }
1740
+ }
1741
+
1742
+ if (!component) {
1743
+ console.error(`❌ Component ${componentName} not found in registry`);
1744
+ return; // Skip this element
1745
+ }
1746
+
1747
+ if (component) {
1748
+ try {
1749
+ // Extract props from data attributes
1750
+ const propsAttr = element.getAttribute('data-coherent-props');
1751
+ const props = propsAttr ? JSON.parse(propsAttr) : {};
1752
+
1753
+ // Extract initial state
1754
+ const stateAttr = element.getAttribute('data-coherent-state');
1755
+ const initialState = stateAttr ? JSON.parse(stateAttr) : null;
1756
+
1757
+ // Hydrate the component
1758
+ const instance = hydrate(element, component, props, { initialState });
1759
+
1760
+ if (instance) {
1761
+ // Component auto-hydrated successfully
1762
+ } else {
1763
+ console.warn(`❌ Failed to hydrate component: ${componentName}`);
1764
+ }
1765
+ } catch (_error) {
1766
+ console.error(`❌ Failed to auto-hydrate component ${componentName}:`, _error);
1767
+ }
1768
+ }
1769
+ });
1770
+
1771
+ // Also enable client events for any remaining elements
1772
+ enableClientEvents();
1773
+ };
1774
+
1775
+ // Run hydration when DOM is ready
1776
+ if (document.readyState === 'loading') {
1777
+ document.addEventListener('DOMContentLoaded', hydrateComponents);
1778
+ } else {
1779
+ hydrateComponents();
1780
+ }
1781
+ }
1782
+
1783
+ // Also export individual functions for convenience
1784
+ export {
1785
+ hydrate,
1786
+ hydrateAll,
1787
+ hydrateBySelector,
1788
+ enableClientEvents,
1789
+ makeHydratable,
1790
+ autoHydrate
1791
+ };