eleva 1.0.0-alpha → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/eleva.esm.js CHANGED
@@ -1,70 +1,101 @@
1
+ /*! Eleva v1.0.0-rc.1 | MIT License | https://elevajs.com */
1
2
  /**
2
- * 🔒 TemplateEngine: Secure interpolation & dynamic attribute parsing.
3
+ * @class 🔒 TemplateEngine
4
+ * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
5
+ * Provides a safe way to evaluate expressions in templates while preventing XSS attacks.
6
+ * All methods are static and can be called directly on the class.
3
7
  *
4
- * This class provides methods to parse template strings by replacing
5
- * interpolation expressions with dynamic data values and to evaluate expressions
6
- * within a given data context.
8
+ * @example
9
+ * const template = "Hello, {{name}}!";
10
+ * const data = { name: "World" };
11
+ * const result = TemplateEngine.parse(template, data); // Returns: "Hello, World!"
7
12
  */
8
13
  class TemplateEngine {
9
14
  /**
10
- * Parses a template string and replaces interpolation expressions with corresponding values.
15
+ * @private {RegExp} Regular expression for matching template expressions in the format {{ expression }}
16
+ * @type {RegExp}
17
+ */
18
+ static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
19
+
20
+ /**
21
+ * Parses a template string, replacing expressions with their evaluated values.
22
+ * Expressions are evaluated in the provided data context.
11
23
  *
12
- * @param {string} template - The template string containing expressions in the format {{ expression }}.
13
- * @param {object} data - The data object to use for evaluating expressions.
14
- * @returns {string} The resulting string with evaluated values.
24
+ * @public
25
+ * @static
26
+ * @param {string} template - The template string to parse.
27
+ * @param {Record<string, unknown>} data - The data context for evaluating expressions.
28
+ * @returns {string} The parsed template with expressions replaced by their values.
29
+ * @example
30
+ * const result = TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
31
+ * user: { name: "John", age: 30 }
32
+ * }); // Returns: "John is 30 years old"
15
33
  */
16
34
  static parse(template, data) {
17
- return template.replace(/\{\{\s*(.*?)\s*\}\}/g, (_, expr) => {
18
- const value = this.evaluate(expr, data);
19
- return value === undefined ? "" : value;
20
- });
35
+ if (typeof template !== "string") return template;
36
+ return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
21
37
  }
22
38
 
23
39
  /**
24
- * Evaluates an expression using the provided data context.
40
+ * Evaluates an expression in the context of the provided data object.
41
+ * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
42
+ * The use of the `with` statement is necessary for expression evaluation but has security implications.
43
+ * Expressions should be carefully validated before evaluation.
25
44
  *
26
- * @param {string} expr - The JavaScript expression to evaluate.
27
- * @param {object} data - The data context for evaluating the expression.
28
- * @returns {*} The result of the evaluated expression, or an empty string if undefined or on error.
45
+ * @public
46
+ * @static
47
+ * @param {string} expression - The expression to evaluate.
48
+ * @param {Record<string, unknown>} data - The data context for evaluation.
49
+ * @returns {unknown} The result of the evaluation, or an empty string if evaluation fails.
50
+ * @example
51
+ * const result = TemplateEngine.evaluate("user.name", { user: { name: "John" } }); // Returns: "John"
52
+ * const age = TemplateEngine.evaluate("user.age", { user: { age: 30 } }); // Returns: 30
29
53
  */
30
- static evaluate(expr, data) {
54
+ static evaluate(expression, data) {
55
+ if (typeof expression !== "string") return expression;
31
56
  try {
32
- const keys = Object.keys(data);
33
- const values = keys.map(k => data[k]);
34
- const result = new Function(...keys, `return ${expr}`)(...values);
35
- return result === undefined ? "" : result;
36
- } catch (error) {
37
- console.error(`Template evaluation error:`, {
38
- expression: expr,
39
- data,
40
- error: error.message
41
- });
57
+ return new Function("data", `with(data) { return ${expression}; }`)(data);
58
+ } catch {
42
59
  return "";
43
60
  }
44
61
  }
45
62
  }
46
63
 
47
64
  /**
48
- * ⚡ Signal: Fine-grained reactivity.
65
+ * @class ⚡ Signal
66
+ * @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
67
+ * Signals notify registered watchers when their value changes, enabling efficient DOM updates
68
+ * through targeted patching rather than full re-renders.
69
+ * Updates are batched using microtasks to prevent multiple synchronous notifications.
70
+ * The class is generic, allowing type-safe handling of any value type T.
49
71
  *
50
- * A reactive data holder that notifies registered watchers when its value changes,
51
- * allowing for fine-grained DOM patching rather than full re-renders.
72
+ * @example
73
+ * const count = new Signal(0);
74
+ * count.watch((value) => console.log(`Count changed to: ${value}`));
75
+ * count.value = 1; // Logs: "Count changed to: 1"
76
+ * @template T
52
77
  */
53
78
  class Signal {
54
79
  /**
55
- * Creates a new Signal instance.
80
+ * Creates a new Signal instance with the specified initial value.
56
81
  *
57
- * @param {*} value - The initial value of the signal.
82
+ * @public
83
+ * @param {T} value - The initial value of the signal.
58
84
  */
59
85
  constructor(value) {
86
+ /** @private {T} Internal storage for the signal's current value */
60
87
  this._value = value;
88
+ /** @private {Set<(value: T) => void>} Collection of callback functions to be notified when value changes */
61
89
  this._watchers = new Set();
90
+ /** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
91
+ this._pending = false;
62
92
  }
63
93
 
64
94
  /**
65
95
  * Gets the current value of the signal.
66
96
  *
67
- * @returns {*} The current value.
97
+ * @public
98
+ * @returns {T} The current value.
68
99
  */
69
100
  get value() {
70
101
  return this._value;
@@ -72,417 +103,866 @@ class Signal {
72
103
 
73
104
  /**
74
105
  * Sets a new value for the signal and notifies all registered watchers if the value has changed.
106
+ * The notification is batched using microtasks to prevent multiple synchronous updates.
75
107
  *
76
- * @param {*} newVal - The new value to set.
108
+ * @public
109
+ * @param {T} newVal - The new value to set.
110
+ * @returns {void}
77
111
  */
78
112
  set value(newVal) {
79
- if (newVal !== this._value) {
80
- this._value = newVal;
81
- this._watchers.forEach(fn => fn(newVal));
82
- }
113
+ if (this._value === newVal) return;
114
+ this._value = newVal;
115
+ this._notify();
83
116
  }
84
117
 
85
118
  /**
86
119
  * Registers a watcher function that will be called whenever the signal's value changes.
120
+ * The watcher will receive the new value as its argument.
87
121
  *
88
- * @param {Function} fn - The callback function to invoke on value change.
89
- * @returns {Function} A function to unsubscribe the watcher.
122
+ * @public
123
+ * @param {(value: T) => void} fn - The callback function to invoke on value change.
124
+ * @returns {() => boolean} A function to unsubscribe the watcher.
125
+ * @example
126
+ * const unsubscribe = signal.watch((value) => console.log(value));
127
+ * // Later...
128
+ * unsubscribe(); // Stops watching for changes
90
129
  */
91
130
  watch(fn) {
92
131
  this._watchers.add(fn);
93
132
  return () => this._watchers.delete(fn);
94
133
  }
134
+
135
+ /**
136
+ * Notifies all registered watchers of a value change using microtask scheduling.
137
+ * Uses a pending flag to batch multiple synchronous updates into a single notification.
138
+ * All watcher callbacks receive the current value when executed.
139
+ *
140
+ * @private
141
+ * @returns {void}
142
+ */
143
+ _notify() {
144
+ if (this._pending) return;
145
+ this._pending = true;
146
+ queueMicrotask(() => {
147
+ /** @type {(fn: (value: T) => void) => void} */
148
+ this._watchers.forEach(fn => fn(this._value));
149
+ this._pending = false;
150
+ });
151
+ }
95
152
  }
96
153
 
97
154
  /**
98
- * 🎙️ Emitter: Robust inter-component communication with event bubbling.
155
+ * @class 📡 Emitter
156
+ * @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
157
+ * Components can emit events and listen for events from other components, facilitating loose coupling
158
+ * and reactive updates across the application.
159
+ * Events are handled synchronously in the order they were registered, with proper cleanup
160
+ * of unsubscribed handlers.
161
+ * Event names should follow the format 'namespace:action' (e.g., 'user:login', 'cart:update').
99
162
  *
100
- * Implements a basic publish-subscribe pattern for event handling,
101
- * allowing components to communicate through custom events.
163
+ * @example
164
+ * const emitter = new Emitter();
165
+ * emitter.on('user:login', (user) => console.log(`User logged in: ${user.name}`));
166
+ * emitter.emit('user:login', { name: 'John' }); // Logs: "User logged in: John"
102
167
  */
103
168
  class Emitter {
104
169
  /**
105
170
  * Creates a new Emitter instance.
171
+ *
172
+ * @public
106
173
  */
107
174
  constructor() {
108
- /** @type {Object.<string, Function[]>} */
109
- this.events = {};
175
+ /** @private {Map<string, Set<(data: unknown) => void>>} Map of event names to their registered handler functions */
176
+ this._events = new Map();
110
177
  }
111
178
 
112
179
  /**
113
- * Registers an event handler for the specified event.
180
+ * Registers an event handler for the specified event name.
181
+ * The handler will be called with the event data when the event is emitted.
182
+ * Event names should follow the format 'namespace:action' for consistency.
114
183
  *
115
- * @param {string} event - The name of the event.
116
- * @param {Function} handler - The function to call when the event is emitted.
184
+ * @public
185
+ * @param {string} event - The name of the event to listen for (e.g., 'user:login').
186
+ * @param {(data: unknown) => void} handler - The callback function to invoke when the event occurs.
187
+ * @returns {() => void} A function to unsubscribe the event handler.
188
+ * @example
189
+ * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
190
+ * // Later...
191
+ * unsubscribe(); // Stops listening for the event
117
192
  */
118
193
  on(event, handler) {
119
- (this.events[event] || (this.events[event] = [])).push(handler);
194
+ if (!this._events.has(event)) this._events.set(event, new Set());
195
+ this._events.get(event).add(handler);
196
+ return () => this.off(event, handler);
120
197
  }
121
198
 
122
199
  /**
123
- * Removes a previously registered event handler.
200
+ * Removes an event handler for the specified event name.
201
+ * If no handler is provided, all handlers for the event are removed.
202
+ * Automatically cleans up empty event sets to prevent memory leaks.
124
203
  *
125
- * @param {string} event - The name of the event.
126
- * @param {Function} handler - The handler function to remove.
204
+ * @public
205
+ * @param {string} event - The name of the event to remove handlers from.
206
+ * @param {(data: unknown) => void} [handler] - The specific handler function to remove.
207
+ * @returns {void}
208
+ * @example
209
+ * // Remove a specific handler
210
+ * emitter.off('user:login', loginHandler);
211
+ * // Remove all handlers for an event
212
+ * emitter.off('user:login');
127
213
  */
128
214
  off(event, handler) {
129
- if (this.events[event]) {
130
- this.events[event] = this.events[event].filter(h => h !== handler);
215
+ if (!this._events.has(event)) return;
216
+ if (handler) {
217
+ const handlers = this._events.get(event);
218
+ handlers.delete(handler);
219
+ // Remove the event if there are no handlers left
220
+ if (handlers.size === 0) this._events.delete(event);
221
+ } else {
222
+ this._events.delete(event);
131
223
  }
132
224
  }
133
225
 
134
226
  /**
135
- * Emits an event, invoking all handlers registered for that event.
227
+ * Emits an event with the specified data to all registered handlers.
228
+ * Handlers are called synchronously in the order they were registered.
229
+ * If no handlers are registered for the event, the emission is silently ignored.
136
230
  *
137
- * @param {string} event - The event name.
138
- * @param {...*} args - Additional arguments to pass to the event handlers.
231
+ * @public
232
+ * @param {string} event - The name of the event to emit.
233
+ * @param {...unknown} args - Optional arguments to pass to the event handlers.
234
+ * @returns {void}
235
+ * @example
236
+ * // Emit an event with data
237
+ * emitter.emit('user:login', { name: 'John', role: 'admin' });
238
+ * // Emit an event with multiple arguments
239
+ * emitter.emit('cart:update', { items: [] }, { total: 0 });
139
240
  */
140
241
  emit(event, ...args) {
141
- (this.events[event] || []).forEach(handler => handler(...args));
242
+ if (!this._events.has(event)) return;
243
+ this._events.get(event).forEach(handler => handler(...args));
142
244
  }
143
245
  }
144
246
 
145
247
  /**
146
- * 🎨 Renderer: Handles DOM patching, diffing, and attribute updates.
248
+ * A regular expression to match hyphenated lowercase letters.
249
+ * @private
250
+ * @type {RegExp}
251
+ */
252
+ const CAMEL_RE = /-([a-z])/g;
253
+
254
+ /**
255
+ * @class 🎨 Renderer
256
+ * @classdesc A high-performance DOM renderer that implements an optimized direct DOM diffing algorithm.
257
+ *
258
+ * Key features:
259
+ * - Single-pass diffing algorithm for efficient DOM updates
260
+ * - Key-based node reconciliation for optimal performance
261
+ * - Intelligent attribute handling for ARIA, data attributes, and boolean properties
262
+ * - Preservation of special Eleva-managed instances and style elements
263
+ * - Memory-efficient with reusable temporary containers
147
264
  *
148
- * Provides methods for efficient DOM updates by diffing the new and old DOM structures
149
- * and applying only the necessary changes.
265
+ * The renderer is designed to minimize DOM operations while maintaining
266
+ * exact attribute synchronization and proper node identity preservation.
267
+ * It's particularly optimized for frequent updates and complex DOM structures.
268
+ *
269
+ * @example
270
+ * const renderer = new Renderer();
271
+ * const container = document.getElementById("app");
272
+ * const newHtml = "<div>Updated content</div>";
273
+ * renderer.patchDOM(container, newHtml);
150
274
  */
151
275
  class Renderer {
152
276
  /**
153
- * Patches the DOM of a container element with new HTML content.
277
+ * Creates a new Renderer instance.
278
+ * @public
279
+ */
280
+ constructor() {
281
+ /**
282
+ * A temporary container to hold the new HTML content while diffing.
283
+ * @private
284
+ * @type {HTMLElement}
285
+ */
286
+ this._tempContainer = document.createElement("div");
287
+ }
288
+
289
+ /**
290
+ * Patches the DOM of the given container with the provided HTML string.
154
291
  *
292
+ * @public
155
293
  * @param {HTMLElement} container - The container element to patch.
156
- * @param {string} newHtml - The new HTML content to apply.
294
+ * @param {string} newHtml - The new HTML string.
295
+ * @returns {void}
296
+ * @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
297
+ * @throws {Error} If DOM patching fails.
157
298
  */
158
299
  patchDOM(container, newHtml) {
159
- const tempContainer = document.createElement("div");
160
- tempContainer.innerHTML = newHtml;
161
- this.diff(container, tempContainer);
300
+ if (!(container instanceof HTMLElement)) {
301
+ throw new TypeError("Container must be an HTMLElement");
302
+ }
303
+ if (typeof newHtml !== "string") {
304
+ throw new TypeError("newHtml must be a string");
305
+ }
306
+ try {
307
+ this._tempContainer.innerHTML = newHtml;
308
+ this._diff(container, this._tempContainer);
309
+ } catch (error) {
310
+ throw new Error(`Failed to patch DOM: ${error.message}`);
311
+ }
162
312
  }
163
313
 
164
314
  /**
165
- * Diffs two DOM trees (old and new) and applies updates to the old DOM.
315
+ * Performs a diff between two DOM nodes and patches the old node to match the new node.
166
316
  *
317
+ * @private
167
318
  * @param {HTMLElement} oldParent - The original DOM element.
168
319
  * @param {HTMLElement} newParent - The new DOM element.
320
+ * @returns {void}
169
321
  */
170
- diff(oldParent, newParent) {
171
- const oldNodes = Array.from(oldParent.childNodes);
172
- const newNodes = Array.from(newParent.childNodes);
173
- const max = Math.max(oldNodes.length, newNodes.length);
174
- for (let i = 0; i < max; i++) {
175
- const oldNode = oldNodes[i];
176
- const newNode = newNodes[i];
177
-
178
- // Append new nodes that don't exist in the old tree.
179
- if (!oldNode && newNode) {
180
- oldParent.appendChild(newNode.cloneNode(true));
181
- continue;
322
+ _diff(oldParent, newParent) {
323
+ if (oldParent === newParent || oldParent.isEqualNode?.(newParent)) return;
324
+ const oldChildren = Array.from(oldParent.childNodes);
325
+ const newChildren = Array.from(newParent.childNodes);
326
+ let oldStartIdx = 0,
327
+ newStartIdx = 0;
328
+ let oldEndIdx = oldChildren.length - 1;
329
+ let newEndIdx = newChildren.length - 1;
330
+ let oldKeyMap = null;
331
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
332
+ let oldStartNode = oldChildren[oldStartIdx];
333
+ let newStartNode = newChildren[newStartIdx];
334
+ if (!oldStartNode) {
335
+ oldStartNode = oldChildren[++oldStartIdx];
336
+ } else if (this._isSameNode(oldStartNode, newStartNode)) {
337
+ this._patchNode(oldStartNode, newStartNode);
338
+ oldStartIdx++;
339
+ newStartIdx++;
340
+ } else {
341
+ if (!oldKeyMap) {
342
+ oldKeyMap = this._createKeyMap(oldChildren, oldStartIdx, oldEndIdx);
343
+ }
344
+ const key = this._getNodeKey(newStartNode);
345
+ const oldNodeToMove = key ? oldKeyMap.get(key) : null;
346
+ if (oldNodeToMove) {
347
+ this._patchNode(oldNodeToMove, newStartNode);
348
+ oldParent.insertBefore(oldNodeToMove, oldStartNode);
349
+ oldChildren[oldChildren.indexOf(oldNodeToMove)] = null;
350
+ } else {
351
+ oldParent.insertBefore(newStartNode.cloneNode(true), oldStartNode);
352
+ }
353
+ newStartIdx++;
182
354
  }
183
- // Remove old nodes not present in the new tree.
184
- if (oldNode && !newNode) {
185
- oldParent.removeChild(oldNode);
186
- continue;
355
+ }
356
+ if (oldStartIdx > oldEndIdx) {
357
+ const refNode = newChildren[newEndIdx + 1] ? oldChildren[oldStartIdx] : null;
358
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
359
+ if (newChildren[i]) oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
187
360
  }
361
+ } else if (newStartIdx > newEndIdx) {
362
+ for (let i = oldStartIdx; i <= oldEndIdx; i++) {
363
+ if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
364
+ }
365
+ }
366
+ }
188
367
 
189
- // For element nodes, compare keys if available.
190
- if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
191
- const oldKey = oldNode.getAttribute("key");
192
- const newKey = newNode.getAttribute("key");
193
- if (oldKey || newKey) {
194
- if (oldKey !== newKey) {
195
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
196
- continue;
368
+ /**
369
+ * Patches a single node.
370
+ *
371
+ * @private
372
+ * @param {Node} oldNode - The original DOM node.
373
+ * @param {Node} newNode - The new DOM node.
374
+ * @returns {void}
375
+ */
376
+ _patchNode(oldNode, newNode) {
377
+ if (oldNode?._eleva_instance) return;
378
+ if (!this._isSameNode(oldNode, newNode)) {
379
+ oldNode.replaceWith(newNode.cloneNode(true));
380
+ return;
381
+ }
382
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
383
+ this._updateAttributes(oldNode, newNode);
384
+ this._diff(oldNode, newNode);
385
+ } else if (oldNode.nodeType === Node.TEXT_NODE && oldNode.nodeValue !== newNode.nodeValue) {
386
+ oldNode.nodeValue = newNode.nodeValue;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Removes a node from its parent.
392
+ *
393
+ * @private
394
+ * @param {HTMLElement} parent - The parent element containing the node to remove.
395
+ * @param {Node} node - The node to remove.
396
+ * @returns {void}
397
+ */
398
+ _removeNode(parent, node) {
399
+ if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
400
+ parent.removeChild(node);
401
+ }
402
+
403
+ /**
404
+ * Updates the attributes of an element to match a new element's attributes.
405
+ *
406
+ * @private
407
+ * @param {HTMLElement} oldEl - The original element to update.
408
+ * @param {HTMLElement} newEl - The new element to update.
409
+ * @returns {void}
410
+ */
411
+ _updateAttributes(oldEl, newEl) {
412
+ const oldAttrs = oldEl.attributes;
413
+ const newAttrs = newEl.attributes;
414
+
415
+ // Single pass for new/updated attributes
416
+ for (let i = 0; i < newAttrs.length; i++) {
417
+ const {
418
+ name,
419
+ value
420
+ } = newAttrs[i];
421
+ if (name.startsWith("@")) continue;
422
+ if (oldEl.getAttribute(name) === value) continue;
423
+ oldEl.setAttribute(name, value);
424
+ if (name.startsWith("aria-")) {
425
+ const prop = "aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
426
+ oldEl[prop] = value;
427
+ } else if (name.startsWith("data-")) {
428
+ oldEl.dataset[name.slice(5)] = value;
429
+ } else {
430
+ const prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
431
+ if (prop in oldEl) {
432
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop);
433
+ const isBoolean = typeof oldEl[prop] === "boolean" || descriptor?.get && typeof descriptor.get.call(oldEl) === "boolean";
434
+ if (isBoolean) {
435
+ oldEl[prop] = value !== "false" && (value === "" || value === prop || value === "true");
436
+ } else {
437
+ oldEl[prop] = value;
197
438
  }
198
439
  }
199
440
  }
441
+ }
200
442
 
201
- // Replace nodes if types or tag names differ.
202
- if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
203
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
204
- continue;
205
- }
206
- // For text nodes, update content if different.
207
- if (oldNode.nodeType === Node.TEXT_NODE) {
208
- if (oldNode.nodeValue !== newNode.nodeValue) {
209
- oldNode.nodeValue = newNode.nodeValue;
210
- }
211
- continue;
212
- }
213
- // For element nodes, update attributes and then diff children.
214
- if (oldNode.nodeType === Node.ELEMENT_NODE) {
215
- this.updateAttributes(oldNode, newNode);
216
- this.diff(oldNode, newNode);
443
+ // Remove any attributes no longer present
444
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
445
+ const name = oldAttrs[i].name;
446
+ if (!newEl.hasAttribute(name)) {
447
+ oldEl.removeAttribute(name);
217
448
  }
218
449
  }
219
450
  }
220
451
 
221
452
  /**
222
- * Updates the attributes of an element to match those of a new element.
453
+ * Determines if two nodes are the same based on their type, name, and key attributes.
223
454
  *
224
- * @param {HTMLElement} oldEl - The element to update.
225
- * @param {HTMLElement} newEl - The element providing the updated attributes.
455
+ * @private
456
+ * @param {Node} oldNode - The first node to compare.
457
+ * @param {Node} newNode - The second node to compare.
458
+ * @returns {boolean} True if the nodes are considered the same, false otherwise.
226
459
  */
227
- updateAttributes(oldEl, newEl) {
228
- const attributeToPropertyMap = {
229
- value: "value",
230
- checked: "checked",
231
- selected: "selected",
232
- disabled: "disabled"
233
- };
460
+ _isSameNode(oldNode, newNode) {
461
+ if (!oldNode || !newNode) return false;
462
+ const oldKey = oldNode.nodeType === Node.ELEMENT_NODE ? oldNode.getAttribute("key") : null;
463
+ const newKey = newNode.nodeType === Node.ELEMENT_NODE ? newNode.getAttribute("key") : null;
464
+ if (oldKey && newKey) return oldKey === newKey;
465
+ return !oldKey && !newKey && oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName;
466
+ }
234
467
 
235
- // Remove old attributes that no longer exist.
236
- Array.from(oldEl.attributes).forEach(attr => {
237
- if (attr.name.startsWith("@")) return;
238
- if (!newEl.hasAttribute(attr.name)) {
239
- oldEl.removeAttribute(attr.name);
240
- }
241
- });
242
- // Add or update attributes from newEl.
243
- Array.from(newEl.attributes).forEach(attr => {
244
- if (attr.name.startsWith("@")) return;
245
- if (oldEl.getAttribute(attr.name) !== attr.value) {
246
- oldEl.setAttribute(attr.name, attr.value);
247
- if (attributeToPropertyMap[attr.name]) {
248
- oldEl[attributeToPropertyMap[attr.name]] = attr.value;
249
- } else if (attr.name in oldEl) {
250
- oldEl[attr.name] = attr.value;
251
- }
252
- }
253
- });
468
+ /**
469
+ * Creates a key map for the children of a parent node.
470
+ *
471
+ * @private
472
+ * @param {Array<Node>} children - The children of the parent node.
473
+ * @param {number} start - The start index of the children.
474
+ * @param {number} end - The end index of the children.
475
+ * @returns {Map<string, Node>} A key map for the children.
476
+ */
477
+ _createKeyMap(children, start, end) {
478
+ const map = new Map();
479
+ for (let i = start; i <= end; i++) {
480
+ const child = children[i];
481
+ const key = this._getNodeKey(child);
482
+ if (key) map.set(key, child);
483
+ }
484
+ return map;
485
+ }
486
+
487
+ /**
488
+ * Extracts the key attribute from a node if it exists.
489
+ *
490
+ * @private
491
+ * @param {Node} node - The node to extract the key from.
492
+ * @returns {string|null} The key attribute value or null if not found.
493
+ */
494
+ _getNodeKey(node) {
495
+ return node?.nodeType === Node.ELEMENT_NODE ? node.getAttribute("key") : null;
254
496
  }
255
497
  }
256
498
 
257
499
  /**
258
- * 🧩 Eleva Core: Signal-based component runtime framework with lifecycle, scoped styles, and plugins.
500
+ * @typedef {Object} ComponentDefinition
501
+ * @property {function(ComponentContext): (Record<string, unknown>|Promise<Record<string, unknown>>)} [setup]
502
+ * Optional setup function that initializes the component's state and returns reactive data
503
+ * @property {(function(ComponentContext): string|Promise<string>)} template
504
+ * Required function that defines the component's HTML structure
505
+ * @property {(function(ComponentContext): string)|string} [style]
506
+ * Optional function or string that provides component-scoped CSS styles
507
+ * @property {Record<string, ComponentDefinition>} [children]
508
+ * Optional object defining nested child components
509
+ */
510
+
511
+ /**
512
+ * @typedef {Object} ComponentContext
513
+ * @property {Record<string, unknown>} props
514
+ * Component properties passed during mounting
515
+ * @property {Emitter} emitter
516
+ * Event emitter instance for component event handling
517
+ * @property {function<T>(value: T): Signal<T>} signal
518
+ * Factory function to create reactive Signal instances
519
+ * @property {function(LifecycleHookContext): Promise<void>} [onBeforeMount]
520
+ * Hook called before component mounting
521
+ * @property {function(LifecycleHookContext): Promise<void>} [onMount]
522
+ * Hook called after component mounting
523
+ * @property {function(LifecycleHookContext): Promise<void>} [onBeforeUpdate]
524
+ * Hook called before component update
525
+ * @property {function(LifecycleHookContext): Promise<void>} [onUpdate]
526
+ * Hook called after component update
527
+ * @property {function(UnmountHookContext): Promise<void>} [onUnmount]
528
+ * Hook called during component unmounting
529
+ */
530
+
531
+ /**
532
+ * @typedef {Object} LifecycleHookContext
533
+ * @property {HTMLElement} container
534
+ * The DOM element where the component is mounted
535
+ * @property {ComponentContext} context
536
+ * The component's reactive state and context data
537
+ */
538
+
539
+ /**
540
+ * @typedef {Object} UnmountHookContext
541
+ * @property {HTMLElement} container
542
+ * The DOM element where the component is mounted
543
+ * @property {ComponentContext} context
544
+ * The component's reactive state and context data
545
+ * @property {{
546
+ * watchers: Array<() => void>, // Signal watcher cleanup functions
547
+ * listeners: Array<() => void>, // Event listener cleanup functions
548
+ * children: Array<MountResult> // Child component instances
549
+ * }} cleanup
550
+ * Object containing cleanup functions and instances
551
+ */
552
+
553
+ /**
554
+ * @typedef {Object} MountResult
555
+ * @property {HTMLElement} container
556
+ * The DOM element where the component is mounted
557
+ * @property {ComponentContext} data
558
+ * The component's reactive state and context data
559
+ * @property {function(): Promise<void>} unmount
560
+ * Function to clean up and unmount the component
561
+ */
562
+
563
+ /**
564
+ * @typedef {Object} ElevaPlugin
565
+ * @property {function(Eleva, Record<string, unknown>): void} install
566
+ * Function that installs the plugin into the Eleva instance
567
+ * @property {string} name
568
+ * Unique identifier name for the plugin
569
+ */
570
+
571
+ /**
572
+ * @class 🧩 Eleva
573
+ * @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
574
+ * scoped styles, and plugin support. Eleva manages component registration, plugin integration,
575
+ * event handling, and DOM rendering with a focus on performance and developer experience.
576
+ *
577
+ * @example
578
+ * // Basic component creation and mounting
579
+ * const app = new Eleva("myApp");
580
+ * app.component("myComponent", {
581
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
582
+ * template: (ctx) => `<div>Hello ${ctx.props.name}</div>`
583
+ * });
584
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
259
585
  *
260
- * The Eleva class is the core of the framework. It manages component registration,
261
- * plugin integration, lifecycle hooks, event handling, and DOM rendering.
586
+ * @example
587
+ * // Using lifecycle hooks
588
+ * app.component("lifecycleDemo", {
589
+ * setup: () => {
590
+ * return {
591
+ * onMount: ({ container, context }) => {
592
+ * console.log('Component mounted!');
593
+ * }
594
+ * };
595
+ * },
596
+ * template: `<div>Lifecycle Demo</div>`
597
+ * });
262
598
  */
263
599
  class Eleva {
264
600
  /**
265
- * Creates a new Eleva instance.
601
+ * Creates a new Eleva instance with the specified name and configuration.
602
+ *
603
+ * @public
604
+ * @param {string} name - The unique identifier name for this Eleva instance.
605
+ * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
606
+ * May include framework-wide settings and default behaviors.
607
+ * @throws {Error} If the name is not provided or is not a string.
608
+ * @returns {Eleva} A new Eleva instance.
609
+ *
610
+ * @example
611
+ * const app = new Eleva("myApp");
612
+ * app.component("myComponent", {
613
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
614
+ * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
615
+ * });
616
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
266
617
  *
267
- * @param {string} name - The name of the Eleva instance.
268
- * @param {object} [config={}] - Optional configuration for the instance.
269
618
  */
270
619
  constructor(name, config = {}) {
620
+ /** @public {string} The unique identifier name for this Eleva instance */
271
621
  this.name = name;
622
+ /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */
272
623
  this.config = config;
273
- this._components = {};
274
- this._plugins = [];
275
- this._lifecycleHooks = ["onBeforeMount", "onMount", "onBeforeUpdate", "onUpdate", "onUnmount"];
276
- this._isMounted = false;
624
+ /** @public {Emitter} Instance of the event emitter for handling component events */
277
625
  this.emitter = new Emitter();
626
+ /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
627
+ this.signal = Signal;
628
+ /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
278
629
  this.renderer = new Renderer();
630
+
631
+ /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */
632
+ this._components = new Map();
633
+ /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
634
+ this._plugins = new Map();
635
+ /** @private {boolean} Flag indicating if the root component is currently mounted */
636
+ this._isMounted = false;
279
637
  }
280
638
 
281
639
  /**
282
640
  * Integrates a plugin with the Eleva framework.
641
+ * The plugin's install function will be called with the Eleva instance and provided options.
642
+ * After installation, the plugin will be available for use by components.
283
643
  *
284
- * @param {object} [plugin] - The plugin object which should have an install function.
285
- * @param {object} [options={}] - Optional options to pass to the plugin.
286
- * @returns {Eleva} The Eleva instance (for chaining).
644
+ * @public
645
+ * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
646
+ * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
647
+ * @returns {Eleva} The Eleva instance (for method chaining).
648
+ * @example
649
+ * app.use(myPlugin, { option1: "value1" });
287
650
  */
288
651
  use(plugin, options = {}) {
289
- if (typeof plugin.install === "function") {
290
- plugin.install(this, options);
291
- }
292
- this._plugins.push(plugin);
652
+ plugin.install(this, options);
653
+ this._plugins.set(plugin.name, plugin);
293
654
  return this;
294
655
  }
295
656
 
296
657
  /**
297
- * Registers a component with the Eleva instance.
658
+ * Registers a new component with the Eleva instance.
659
+ * The component will be available for mounting using its registered name.
298
660
  *
299
- * @param {string} name - The name of the component.
300
- * @param {object} definition - The component definition including setup, template, style, and children.
301
- * @returns {Eleva} The Eleva instance (for chaining).
661
+ * @public
662
+ * @param {string} name - The unique name of the component to register.
663
+ * @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
664
+ * @returns {Eleva} The Eleva instance (for method chaining).
665
+ * @throws {Error} If the component name is already registered.
666
+ * @example
667
+ * app.component("myButton", {
668
+ * template: (ctx) => `<button>${ctx.props.text}</button>`,
669
+ * style: `button { color: blue; }`
670
+ * });
302
671
  */
303
672
  component(name, definition) {
304
- this._components[name] = definition;
673
+ /** @type {Map<string, ComponentDefinition>} */
674
+ this._components.set(name, definition);
305
675
  return this;
306
676
  }
307
677
 
308
678
  /**
309
679
  * Mounts a registered component to a DOM element.
680
+ * This will initialize the component, set up its reactive state, and render it to the DOM.
310
681
  *
311
- * @param {string|HTMLElement} selectorOrElement - A CSS selector string or DOM element where the component will be mounted.
312
- * @param {string} compName - The name of the component to mount.
313
- * @param {object} [props={}] - Optional properties to pass to the component.
314
- * @returns {object|Promise<object>} An object representing the mounted component instance, or a Promise that resolves to it for asynchronous setups.
315
- * @throws Will throw an error if the container or component is not found.
682
+ * @public
683
+ * @param {HTMLElement} container - The DOM element where the component will be mounted.
684
+ * @param {string|ComponentDefinition} compName - The name of the registered component or a direct component definition.
685
+ * @param {Object<string, unknown>} [props={}] - Optional properties to pass to the component.
686
+ * @returns {Promise<MountResult>}
687
+ * A Promise that resolves to an object containing:
688
+ * - container: The mounted component's container element
689
+ * - data: The component's reactive state and context
690
+ * - unmount: Function to clean up and unmount the component
691
+ * @throws {Error} If the container is not found, or component is not registered.
692
+ * @example
693
+ * const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
694
+ * // Later...
695
+ * instance.unmount();
316
696
  */
317
- mount(selectorOrElement, compName, props = {}) {
318
- const container = typeof selectorOrElement === "string" ? document.querySelector(selectorOrElement) : selectorOrElement;
319
- if (!container) throw new Error(`Container not found: ${selectorOrElement}`);
320
- const definition = this._components[compName];
697
+ async mount(container, compName, props = {}) {
698
+ if (!container) throw new Error(`Container not found: ${container}`);
699
+ if (container._eleva_instance) return container._eleva_instance;
700
+
701
+ /** @type {ComponentDefinition} */
702
+ const definition = typeof compName === "string" ? this._components.get(compName) : compName;
321
703
  if (!definition) throw new Error(`Component "${compName}" not registered.`);
704
+
705
+ /**
706
+ * Destructure the component definition to access core functionality.
707
+ * - setup: Optional function for component initialization and state management
708
+ * - template: Required function or string that returns the component's HTML structure
709
+ * - style: Optional function or string for component-scoped CSS styles
710
+ * - children: Optional object defining nested child components
711
+ */
322
712
  const {
323
713
  setup,
324
714
  template,
325
715
  style,
326
716
  children
327
717
  } = definition;
718
+
719
+ /** @type {ComponentContext} */
328
720
  const context = {
329
721
  props,
330
- emit: this.emitter.emit.bind(this.emitter),
331
- on: this.emitter.on.bind(this.emitter),
332
- signal: v => new Signal(v),
333
- ...this._prepareLifecycleHooks()
722
+ emitter: this.emitter,
723
+ /** @type {(v: unknown) => Signal<unknown>} */
724
+ signal: v => new this.signal(v)
334
725
  };
335
726
 
336
727
  /**
337
728
  * Processes the mounting of the component.
729
+ * This function handles:
730
+ * 1. Merging setup data with the component context
731
+ * 2. Setting up reactive watchers
732
+ * 3. Rendering the component
733
+ * 4. Managing component lifecycle
338
734
  *
339
- * @param {object} data - Data returned from the component's setup function.
340
- * @returns {object} An object with the container, merged context data, and an unmount function.
735
+ * @param {Object<string, unknown>} data - Data returned from the component's setup function
736
+ * @returns {Promise<MountResult>} An object containing:
737
+ * - container: The mounted component's container element
738
+ * - data: The component's reactive state and context
739
+ * - unmount: Function to clean up and unmount the component
341
740
  */
342
- const processMount = data => {
741
+ const processMount = async data => {
742
+ /** @type {ComponentContext} */
343
743
  const mergedContext = {
344
744
  ...context,
345
745
  ...data
346
746
  };
347
- const watcherUnsubscribers = [];
747
+ /** @type {Array<() => void>} */
748
+ const watchers = [];
749
+ /** @type {Array<MountResult>} */
348
750
  const childInstances = [];
751
+ /** @type {Array<() => void>} */
752
+ const listeners = [];
753
+
754
+ // Execute before hooks
349
755
  if (!this._isMounted) {
350
- mergedContext.onBeforeMount && mergedContext.onBeforeMount();
756
+ /** @type {LifecycleHookContext} */
757
+ await mergedContext.onBeforeMount?.({
758
+ container,
759
+ context: mergedContext
760
+ });
351
761
  } else {
352
- mergedContext.onBeforeUpdate && mergedContext.onBeforeUpdate();
762
+ /** @type {LifecycleHookContext} */
763
+ await mergedContext.onBeforeUpdate?.({
764
+ container,
765
+ context: mergedContext
766
+ });
353
767
  }
354
768
 
355
769
  /**
356
- * Renders the component by parsing the template, patching the DOM,
357
- * processing events, injecting styles, and mounting child components.
770
+ * Renders the component by:
771
+ * 1. Processing the template
772
+ * 2. Updating the DOM
773
+ * 3. Processing events, injecting styles, and mounting child components.
358
774
  */
359
- const render = () => {
360
- const newHtml = TemplateEngine.parse(template(mergedContext), mergedContext);
775
+ const render = async () => {
776
+ const templateResult = typeof template === "function" ? await template(mergedContext) : template;
777
+ const newHtml = TemplateEngine.parse(templateResult, mergedContext);
361
778
  this.renderer.patchDOM(container, newHtml);
362
- this._processEvents(container, mergedContext);
363
- this._injectStyles(container, compName, style, mergedContext);
364
- this._mountChildren(container, children, childInstances);
779
+ this._processEvents(container, mergedContext, listeners);
780
+ if (style) this._injectStyles(container, compName, style, mergedContext);
781
+ if (children) await this._mountComponents(container, children, childInstances);
365
782
  if (!this._isMounted) {
366
- mergedContext.onMount && mergedContext.onMount();
783
+ /** @type {LifecycleHookContext} */
784
+ await mergedContext.onMount?.({
785
+ container,
786
+ context: mergedContext
787
+ });
367
788
  this._isMounted = true;
368
789
  } else {
369
- mergedContext.onUpdate && mergedContext.onUpdate();
790
+ /** @type {LifecycleHookContext} */
791
+ await mergedContext.onUpdate?.({
792
+ container,
793
+ context: mergedContext
794
+ });
370
795
  }
371
796
  };
372
- Object.values(data).forEach(val => {
373
- if (val instanceof Signal) watcherUnsubscribers.push(val.watch(render));
374
- });
375
- render();
376
- return {
797
+
798
+ /**
799
+ * Sets up reactive watchers for all Signal instances in the component's data.
800
+ * When a Signal's value changes, the component will re-render to reflect the updates.
801
+ * Stores unsubscribe functions to clean up watchers when component unmounts.
802
+ */
803
+ for (const val of Object.values(data)) {
804
+ if (val instanceof Signal) watchers.push(val.watch(render));
805
+ }
806
+ await render();
807
+ const instance = {
377
808
  container,
378
809
  data: mergedContext,
379
810
  /**
380
- * Unmounts the component, cleaning up watchers, child components, and clearing the container.
811
+ * Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
812
+ *
813
+ * @returns {void}
381
814
  */
382
- unmount: () => {
383
- watcherUnsubscribers.forEach(fn => fn());
384
- childInstances.forEach(child => child.unmount());
385
- mergedContext.onUnmount && mergedContext.onUnmount();
815
+ unmount: async () => {
816
+ /** @type {UnmountHookContext} */
817
+ await mergedContext.onUnmount?.({
818
+ container,
819
+ context: mergedContext,
820
+ cleanup: {
821
+ watchers: watchers,
822
+ listeners: listeners,
823
+ children: childInstances
824
+ }
825
+ });
826
+ for (const fn of watchers) fn();
827
+ for (const fn of listeners) fn();
828
+ for (const child of childInstances) await child.unmount();
386
829
  container.innerHTML = "";
830
+ delete container._eleva_instance;
387
831
  }
388
832
  };
833
+ container._eleva_instance = instance;
834
+ return instance;
389
835
  };
390
836
 
391
- // Handle asynchronous setup if needed.
392
- const setupResult = setup(context);
393
- if (setupResult && typeof setupResult.then === "function") {
394
- return setupResult.then(data => processMount(data));
395
- } else {
396
- const data = setupResult || {};
397
- return processMount(data);
398
- }
837
+ // Handle asynchronous setup.
838
+ const setupResult = typeof setup === "function" ? await setup(context) : {};
839
+ return await processMount(setupResult);
399
840
  }
400
841
 
401
842
  /**
402
- * Prepares default no-operation lifecycle hook functions.
843
+ * Processes DOM elements for event binding based on attributes starting with "@".
844
+ * This method handles the event delegation system and ensures proper cleanup of event listeners.
403
845
  *
404
- * @returns {object} An object with keys for lifecycle hooks mapped to empty functions.
405
846
  * @private
847
+ * @param {HTMLElement} container - The container element in which to search for event attributes.
848
+ * @param {ComponentContext} context - The current component context containing event handler definitions.
849
+ * @param {Array<() => void>} listeners - Array to collect cleanup functions for each event listener.
850
+ * @returns {void}
406
851
  */
407
- _prepareLifecycleHooks() {
408
- return this._lifecycleHooks.reduce((acc, hook) => {
409
- acc[hook] = () => {};
410
- return acc;
411
- }, {});
852
+ _processEvents(container, context, listeners) {
853
+ /** @type {NodeListOf<Element>} */
854
+ const elements = container.querySelectorAll("*");
855
+ for (const el of elements) {
856
+ /** @type {NamedNodeMap} */
857
+ const attrs = el.attributes;
858
+ for (let i = 0; i < attrs.length; i++) {
859
+ /** @type {Attr} */
860
+ const attr = attrs[i];
861
+ if (!attr.name.startsWith("@")) continue;
862
+
863
+ /** @type {keyof HTMLElementEventMap} */
864
+ const event = attr.name.slice(1);
865
+ /** @type {string} */
866
+ const handlerName = attr.value;
867
+ /** @type {(event: Event) => void} */
868
+ const handler = context[handlerName] || TemplateEngine.evaluate(handlerName, context);
869
+ if (typeof handler === "function") {
870
+ el.addEventListener(event, handler);
871
+ el.removeAttribute(attr.name);
872
+ listeners.push(() => el.removeEventListener(event, handler));
873
+ }
874
+ }
875
+ }
412
876
  }
413
877
 
414
878
  /**
415
- * Processes DOM elements for event binding based on attributes starting with "@".
879
+ * Injects scoped styles into the component's container.
880
+ * The styles are automatically prefixed to prevent style leakage to other components.
416
881
  *
417
- * @param {HTMLElement} container - The container element in which to search for events.
418
- * @param {object} context - The current context containing event handler definitions.
419
882
  * @private
883
+ * @param {HTMLElement} container - The container element where styles should be injected.
884
+ * @param {string} compName - The component name used to identify the style element.
885
+ * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
886
+ * @param {ComponentContext} context - The current component context for style interpolation.
887
+ * @returns {void}
420
888
  */
421
- _processEvents(container, context) {
422
- container.querySelectorAll("*").forEach(el => {
423
- [...el.attributes].forEach(({
424
- name,
425
- value
426
- }) => {
427
- if (name.startsWith("@")) {
428
- const event = name.slice(1);
429
- const handler = TemplateEngine.evaluate(value, context);
430
- if (typeof handler === "function") {
431
- el.addEventListener(event, handler);
432
- el.removeAttribute(name);
433
- }
434
- }
435
- });
436
- });
889
+ _injectStyles(container, compName, styleDef, context) {
890
+ /** @type {string} */
891
+ const newStyle = typeof styleDef === "function" ? TemplateEngine.parse(styleDef(context), context) : styleDef;
892
+ /** @type {HTMLStyleElement|null} */
893
+ let styleEl = container.querySelector(`style[data-e-style="${compName}"]`);
894
+ if (styleEl && styleEl.textContent === newStyle) return;
895
+ if (!styleEl) {
896
+ styleEl = document.createElement("style");
897
+ styleEl.setAttribute("data-e-style", compName);
898
+ container.appendChild(styleEl);
899
+ }
900
+ styleEl.textContent = newStyle;
437
901
  }
438
902
 
439
903
  /**
440
- * Injects scoped styles into the component's container.
904
+ * Extracts props from an element's attributes that start with the specified prefix.
905
+ * This method is used to collect component properties from DOM elements.
441
906
  *
442
- * @param {HTMLElement} container - The container element.
443
- * @param {string} compName - The component name used to identify the style element.
444
- * @param {Function} styleFn - A function that returns CSS styles as a string.
445
- * @param {object} context - The current context for style interpolation.
446
907
  * @private
908
+ * @param {HTMLElement} element - The DOM element to extract props from
909
+ * @param {string} prefix - The prefix to look for in attributes
910
+ * @returns {Record<string, string>} An object containing the extracted props
911
+ * @example
912
+ * // For an element with attributes:
913
+ * // <div :name="John" :age="25">
914
+ * // Returns: { name: "John", age: "25" }
447
915
  */
448
- _injectStyles(container, compName, styleFn, context) {
449
- if (styleFn) {
450
- let styleEl = container.querySelector(`style[data-eleva-style="${compName}"]`);
451
- if (!styleEl) {
452
- styleEl = document.createElement("style");
453
- styleEl.setAttribute("data-eleva-style", compName);
454
- container.appendChild(styleEl);
916
+ _extractProps(element, prefix) {
917
+ if (!element.attributes) return {};
918
+ const props = {};
919
+ const attrs = element.attributes;
920
+ for (let i = attrs.length - 1; i >= 0; i--) {
921
+ const attr = attrs[i];
922
+ if (attr.name.startsWith(prefix)) {
923
+ const propName = attr.name.slice(prefix.length);
924
+ props[propName] = attr.value;
925
+ element.removeAttribute(attr.name);
455
926
  }
456
- styleEl.textContent = TemplateEngine.parse(styleFn(context), context);
457
927
  }
928
+ return props;
458
929
  }
459
930
 
460
931
  /**
461
- * Mounts child components within the parent component's container.
932
+ * Mounts all components within the parent component's container.
933
+ * This method handles mounting of explicitly defined children components.
934
+ *
935
+ * The mounting process follows these steps:
936
+ * 1. Cleans up any existing component instances
937
+ * 2. Mounts explicitly defined children components
462
938
  *
463
- * @param {HTMLElement} container - The parent container element.
464
- * @param {object} children - An object mapping child component selectors to their definitions.
465
- * @param {Array} childInstances - An array to store the mounted child component instances.
466
939
  * @private
940
+ * @param {HTMLElement} container - The container element to mount components in
941
+ * @param {Object<string, ComponentDefinition>} children - Map of selectors to component definitions for explicit children
942
+ * @param {Array<MountResult>} childInstances - Array to store all mounted component instances
943
+ * @returns {Promise<void>}
944
+ *
945
+ * @example
946
+ * // Explicit children mounting:
947
+ * const children = {
948
+ * 'UserProfile': UserProfileComponent,
949
+ * '#settings-panel': "settings-panel"
950
+ * };
467
951
  */
468
- _mountChildren(container, children, childInstances) {
469
- childInstances.forEach(child => child.unmount());
470
- childInstances.length = 0;
471
- Object.keys(children || {}).forEach(childName => {
472
- container.querySelectorAll(childName).forEach(childEl => {
473
- const props = {};
474
- [...childEl.attributes].forEach(({
475
- name,
476
- value
477
- }) => {
478
- if (name.startsWith("eleva-prop-")) {
479
- props[name.slice("eleva-prop-".length)] = value;
480
- }
481
- });
482
- const instance = this.mount(childEl, childName, props);
483
- childInstances.push(instance);
484
- });
485
- });
952
+ async _mountComponents(container, children, childInstances) {
953
+ for (const [selector, component] of Object.entries(children)) {
954
+ if (!selector) continue;
955
+ for (const el of container.querySelectorAll(selector)) {
956
+ if (!(el instanceof HTMLElement)) continue;
957
+ /** @type {Record<string, string>} */
958
+ const props = this._extractProps(el, ":");
959
+ /** @type {MountResult} */
960
+ const instance = await this.mount(el, component, props);
961
+ if (instance && !childInstances.includes(instance)) {
962
+ childInstances.push(instance);
963
+ }
964
+ }
965
+ }
486
966
  }
487
967
  }
488
968