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