eleva 1.0.0-alpha → 1.0.0-rc.10

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.
Files changed (68) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +554 -137
  3. package/dist/eleva-plugins.cjs.js +3397 -0
  4. package/dist/eleva-plugins.cjs.js.map +1 -0
  5. package/dist/eleva-plugins.esm.js +3392 -0
  6. package/dist/eleva-plugins.esm.js.map +1 -0
  7. package/dist/eleva-plugins.umd.js +3403 -0
  8. package/dist/eleva-plugins.umd.js.map +1 -0
  9. package/dist/eleva-plugins.umd.min.js +3 -0
  10. package/dist/eleva-plugins.umd.min.js.map +1 -0
  11. package/dist/eleva.cjs.js +1448 -0
  12. package/dist/eleva.cjs.js.map +1 -0
  13. package/dist/eleva.d.ts +1057 -80
  14. package/dist/eleva.esm.js +1230 -274
  15. package/dist/eleva.esm.js.map +1 -1
  16. package/dist/eleva.umd.js +1230 -274
  17. package/dist/eleva.umd.js.map +1 -1
  18. package/dist/eleva.umd.min.js +3 -0
  19. package/dist/eleva.umd.min.js.map +1 -0
  20. package/dist/plugins/attr.umd.js +231 -0
  21. package/dist/plugins/attr.umd.js.map +1 -0
  22. package/dist/plugins/attr.umd.min.js +3 -0
  23. package/dist/plugins/attr.umd.min.js.map +1 -0
  24. package/dist/plugins/props.umd.js +711 -0
  25. package/dist/plugins/props.umd.js.map +1 -0
  26. package/dist/plugins/props.umd.min.js +3 -0
  27. package/dist/plugins/props.umd.min.js.map +1 -0
  28. package/dist/plugins/router.umd.js +1807 -0
  29. package/dist/plugins/router.umd.js.map +1 -0
  30. package/dist/plugins/router.umd.min.js +3 -0
  31. package/dist/plugins/router.umd.min.js.map +1 -0
  32. package/dist/plugins/store.umd.js +684 -0
  33. package/dist/plugins/store.umd.js.map +1 -0
  34. package/dist/plugins/store.umd.min.js +3 -0
  35. package/dist/plugins/store.umd.min.js.map +1 -0
  36. package/package.json +240 -62
  37. package/src/core/Eleva.js +552 -145
  38. package/src/modules/Emitter.js +154 -18
  39. package/src/modules/Renderer.js +288 -86
  40. package/src/modules/Signal.js +132 -13
  41. package/src/modules/TemplateEngine.js +153 -27
  42. package/src/plugins/Attr.js +252 -0
  43. package/src/plugins/Props.js +590 -0
  44. package/src/plugins/Router.js +1919 -0
  45. package/src/plugins/Store.js +741 -0
  46. package/src/plugins/index.js +40 -0
  47. package/types/core/Eleva.d.ts +482 -48
  48. package/types/core/Eleva.d.ts.map +1 -1
  49. package/types/modules/Emitter.d.ts +151 -20
  50. package/types/modules/Emitter.d.ts.map +1 -1
  51. package/types/modules/Renderer.d.ts +151 -12
  52. package/types/modules/Renderer.d.ts.map +1 -1
  53. package/types/modules/Signal.d.ts +130 -16
  54. package/types/modules/Signal.d.ts.map +1 -1
  55. package/types/modules/TemplateEngine.d.ts +154 -14
  56. package/types/modules/TemplateEngine.d.ts.map +1 -1
  57. package/types/plugins/Attr.d.ts +28 -0
  58. package/types/plugins/Attr.d.ts.map +1 -0
  59. package/types/plugins/Props.d.ts +48 -0
  60. package/types/plugins/Props.d.ts.map +1 -0
  61. package/types/plugins/Router.d.ts +1000 -0
  62. package/types/plugins/Router.d.ts.map +1 -0
  63. package/types/plugins/Store.d.ts +86 -0
  64. package/types/plugins/Store.d.ts.map +1 -0
  65. package/types/plugins/index.d.ts +5 -0
  66. package/types/plugins/index.d.ts.map +1 -0
  67. package/dist/eleva.min.js +0 -2
  68. package/dist/eleva.min.js.map +0 -1
package/dist/eleva.umd.js CHANGED
@@ -1,76 +1,282 @@
1
+ /*! Eleva v1.0.0-rc.10 | MIT License | https://elevajs.com */
1
2
  (function (global, factory) {
2
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
4
  typeof define === 'function' && define.amd ? define(factory) :
4
5
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Eleva = factory());
5
6
  })(this, (function () { 'use strict';
6
7
 
8
+ // ============================================================================
9
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
10
+ // ============================================================================
11
+
12
+ /**
13
+ * @typedef {Record<string, unknown>} TemplateData
14
+ * Data context for template interpolation
15
+ */
16
+
17
+ /**
18
+ * @typedef {string} TemplateString
19
+ * A string containing {{ expression }} interpolation markers
20
+ */
21
+
22
+ /**
23
+ * @typedef {string} Expression
24
+ * A JavaScript expression to be evaluated in the data context
25
+ */
26
+
7
27
  /**
8
- * 🔒 TemplateEngine: Secure interpolation & dynamic attribute parsing.
28
+ * @typedef {unknown} EvaluationResult
29
+ * The result of evaluating an expression (string, number, boolean, object, etc.)
30
+ */
31
+
32
+ /**
33
+ * @class 🔒 TemplateEngine
34
+ * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
35
+ * Provides a way to evaluate expressions in templates.
36
+ * All methods are static and can be called directly on the class.
37
+ *
38
+ * Template Syntax:
39
+ * - `{{ expression }}` - Interpolate any JavaScript expression
40
+ * - `{{ variable }}` - Access data properties directly
41
+ * - `{{ object.property }}` - Access nested properties
42
+ * - `{{ condition ? a : b }}` - Ternary expressions
43
+ * - `{{ func(arg) }}` - Call functions from data context
44
+ *
45
+ * @example
46
+ * // Basic interpolation
47
+ * const template = "Hello, {{name}}!";
48
+ * const data = { name: "World" };
49
+ * const result = TemplateEngine.parse(template, data);
50
+ * // Result: "Hello, World!"
9
51
  *
10
- * This class provides methods to parse template strings by replacing
11
- * interpolation expressions with dynamic data values and to evaluate expressions
12
- * within a given data context.
52
+ * @example
53
+ * // Nested properties
54
+ * const template = "Welcome, {{user.name}}!";
55
+ * const data = { user: { name: "John" } };
56
+ * const result = TemplateEngine.parse(template, data);
57
+ * // Result: "Welcome, John!"
58
+ *
59
+ * @example
60
+ * // Expressions
61
+ * const template = "Status: {{active ? 'Online' : 'Offline'}}";
62
+ * const data = { active: true };
63
+ * const result = TemplateEngine.parse(template, data);
64
+ * // Result: "Status: Online"
65
+ *
66
+ * @example
67
+ * // With Signal values
68
+ * const template = "Count: {{count.value}}";
69
+ * const data = { count: { value: 42 } };
70
+ * const result = TemplateEngine.parse(template, data);
71
+ * // Result: "Count: 42"
13
72
  */
14
73
  class TemplateEngine {
15
74
  /**
16
- * Parses a template string and replaces interpolation expressions with corresponding values.
75
+ * Regular expression for matching template expressions in the format {{ expression }}
76
+ * Matches: {{ anything }} with optional whitespace inside braces
77
+ *
78
+ * @static
79
+ * @private
80
+ * @type {RegExp}
81
+ */
82
+ static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
83
+
84
+ /**
85
+ * Parses a template string, replacing expressions with their evaluated values.
86
+ * Expressions are evaluated in the provided data context.
87
+ *
88
+ * @public
89
+ * @static
90
+ * @param {TemplateString|unknown} template - The template string to parse.
91
+ * @param {TemplateData} data - The data context for evaluating expressions.
92
+ * @returns {string} The parsed template with expressions replaced by their values.
93
+ *
94
+ * @example
95
+ * // Simple variables
96
+ * TemplateEngine.parse("Hello, {{name}}!", { name: "World" });
97
+ * // Result: "Hello, World!"
98
+ *
99
+ * @example
100
+ * // Nested properties
101
+ * TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
102
+ * user: { name: "John", age: 30 }
103
+ * });
104
+ * // Result: "John is 30 years old"
17
105
  *
18
- * @param {string} template - The template string containing expressions in the format {{ expression }}.
19
- * @param {object} data - The data object to use for evaluating expressions.
20
- * @returns {string} The resulting string with evaluated values.
106
+ * @example
107
+ * // Multiple expressions
108
+ * TemplateEngine.parse("{{greeting}}, {{name}}! You have {{count}} messages.", {
109
+ * greeting: "Hello",
110
+ * name: "User",
111
+ * count: 5
112
+ * });
113
+ * // Result: "Hello, User! You have 5 messages."
114
+ *
115
+ * @example
116
+ * // With conditionals
117
+ * TemplateEngine.parse("Status: {{online ? 'Active' : 'Inactive'}}", {
118
+ * online: true
119
+ * });
120
+ * // Result: "Status: Active"
21
121
  */
22
122
  static parse(template, data) {
23
- return template.replace(/\{\{\s*(.*?)\s*\}\}/g, (_, expr) => {
24
- const value = this.evaluate(expr, data);
25
- return value === undefined ? "" : value;
26
- });
123
+ if (typeof template !== "string") return template;
124
+ return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
27
125
  }
28
126
 
29
127
  /**
30
- * Evaluates an expression using the provided data context.
128
+ * Evaluates an expression in the context of the provided data object.
129
+ *
130
+ * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
131
+ * The use of the `with` statement is necessary for expression evaluation but has security implications.
132
+ * Only use with trusted templates. User input should never be directly interpolated.
133
+ *
134
+ * @public
135
+ * @static
136
+ * @param {Expression|unknown} expression - The expression to evaluate.
137
+ * @param {TemplateData} data - The data context for evaluation.
138
+ * @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
139
+ *
140
+ * @example
141
+ * // Property access
142
+ * TemplateEngine.evaluate("user.name", { user: { name: "John" } });
143
+ * // Result: "John"
31
144
  *
32
- * @param {string} expr - The JavaScript expression to evaluate.
33
- * @param {object} data - The data context for evaluating the expression.
34
- * @returns {*} The result of the evaluated expression, or an empty string if undefined or on error.
145
+ * @example
146
+ * // Numeric values
147
+ * TemplateEngine.evaluate("user.age", { user: { age: 30 } });
148
+ * // Result: 30
149
+ *
150
+ * @example
151
+ * // Expressions
152
+ * TemplateEngine.evaluate("items.length > 0", { items: [1, 2, 3] });
153
+ * // Result: true
154
+ *
155
+ * @example
156
+ * // Function calls
157
+ * TemplateEngine.evaluate("formatDate(date)", {
158
+ * date: new Date(),
159
+ * formatDate: (d) => d.toISOString()
160
+ * });
161
+ * // Result: "2024-01-01T00:00:00.000Z"
162
+ *
163
+ * @example
164
+ * // Failed evaluation returns empty string
165
+ * TemplateEngine.evaluate("nonexistent.property", {});
166
+ * // Result: ""
35
167
  */
36
- static evaluate(expr, data) {
168
+ static evaluate(expression, data) {
169
+ if (typeof expression !== "string") return expression;
37
170
  try {
38
- const keys = Object.keys(data);
39
- const values = keys.map(k => data[k]);
40
- const result = new Function(...keys, `return ${expr}`)(...values);
41
- return result === undefined ? "" : result;
42
- } catch (error) {
43
- console.error(`Template evaluation error:`, {
44
- expression: expr,
45
- data,
46
- error: error.message
47
- });
171
+ return new Function("data", `with(data) { return ${expression}; }`)(data);
172
+ } catch {
48
173
  return "";
49
174
  }
50
175
  }
51
176
  }
52
177
 
178
+ // ============================================================================
179
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
180
+ // ============================================================================
181
+
53
182
  /**
54
- * Signal: Fine-grained reactivity.
183
+ * @template T
184
+ * @callback SignalWatcher
185
+ * @param {T} value - The new value of the signal
186
+ * @returns {void}
187
+ */
188
+
189
+ /**
190
+ * @callback SignalUnsubscribe
191
+ * @returns {boolean} True if the watcher was successfully removed
192
+ */
193
+
194
+ /**
195
+ * @template T
196
+ * @typedef {Object} SignalLike
197
+ * @property {T} value - The current value
198
+ * @property {function(SignalWatcher<T>): SignalUnsubscribe} watch - Subscribe to changes
199
+ */
200
+
201
+ /**
202
+ * @class ⚡ Signal
203
+ * @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
204
+ * Signals notify registered watchers when their value changes, enabling efficient DOM updates
205
+ * through targeted patching rather than full re-renders.
206
+ * Updates are batched using microtasks to prevent multiple synchronous notifications.
207
+ * The class is generic, allowing type-safe handling of any value type T.
208
+ *
209
+ * @template T The type of value held by this signal
55
210
  *
56
- * A reactive data holder that notifies registered watchers when its value changes,
57
- * allowing for fine-grained DOM patching rather than full re-renders.
211
+ * @example
212
+ * // Basic usage
213
+ * const count = new Signal(0);
214
+ * count.watch((value) => console.log(`Count changed to: ${value}`));
215
+ * count.value = 1; // Logs: "Count changed to: 1"
216
+ *
217
+ * @example
218
+ * // With unsubscribe
219
+ * const name = new Signal("John");
220
+ * const unsubscribe = name.watch((value) => console.log(value));
221
+ * name.value = "Jane"; // Logs: "Jane"
222
+ * unsubscribe(); // Stop watching
223
+ * name.value = "Bob"; // No log output
224
+ *
225
+ * @example
226
+ * // With objects
227
+ * /** @type {Signal<{x: number, y: number}>} *\/
228
+ * const position = new Signal({ x: 0, y: 0 });
229
+ * position.value = { x: 10, y: 20 }; // Triggers watchers
230
+ *
231
+ * @implements {SignalLike<T>}
58
232
  */
59
233
  class Signal {
60
234
  /**
61
- * Creates a new Signal instance.
235
+ * Creates a new Signal instance with the specified initial value.
236
+ *
237
+ * @public
238
+ * @param {T} value - The initial value of the signal.
239
+ *
240
+ * @example
241
+ * // Primitive types
242
+ * const count = new Signal(0); // Signal<number>
243
+ * const name = new Signal("John"); // Signal<string>
244
+ * const active = new Signal(true); // Signal<boolean>
62
245
  *
63
- * @param {*} value - The initial value of the signal.
246
+ * @example
247
+ * // Complex types (use JSDoc for type inference)
248
+ * /** @type {Signal<string[]>} *\/
249
+ * const items = new Signal([]);
250
+ *
251
+ * /** @type {Signal<{id: number, name: string} | null>} *\/
252
+ * const user = new Signal(null);
64
253
  */
65
254
  constructor(value) {
255
+ /**
256
+ * Internal storage for the signal's current value
257
+ * @private
258
+ * @type {T}
259
+ */
66
260
  this._value = value;
261
+ /**
262
+ * Collection of callback functions to be notified when value changes
263
+ * @private
264
+ * @type {Set<SignalWatcher<T>>}
265
+ */
67
266
  this._watchers = new Set();
267
+ /**
268
+ * Flag to prevent multiple synchronous watcher notifications
269
+ * @private
270
+ * @type {boolean}
271
+ */
272
+ this._pending = false;
68
273
  }
69
274
 
70
275
  /**
71
276
  * Gets the current value of the signal.
72
277
  *
73
- * @returns {*} The current value.
278
+ * @public
279
+ * @returns {T} The current value.
74
280
  */
75
281
  get value() {
76
282
  return this._value;
@@ -78,417 +284,1167 @@
78
284
 
79
285
  /**
80
286
  * Sets a new value for the signal and notifies all registered watchers if the value has changed.
287
+ * The notification is batched using microtasks to prevent multiple synchronous updates.
81
288
  *
82
- * @param {*} newVal - The new value to set.
289
+ * @public
290
+ * @param {T} newVal - The new value to set.
291
+ * @returns {void}
83
292
  */
84
293
  set value(newVal) {
85
- if (newVal !== this._value) {
86
- this._value = newVal;
87
- this._watchers.forEach(fn => fn(newVal));
88
- }
294
+ if (this._value === newVal) return;
295
+ this._value = newVal;
296
+ this._notify();
89
297
  }
90
298
 
91
299
  /**
92
300
  * Registers a watcher function that will be called whenever the signal's value changes.
301
+ * The watcher will receive the new value as its argument.
302
+ *
303
+ * @public
304
+ * @param {SignalWatcher<T>} fn - The callback function to invoke on value change.
305
+ * @returns {SignalUnsubscribe} A function to unsubscribe the watcher.
306
+ *
307
+ * @example
308
+ * // Basic watching
309
+ * const unsubscribe = signal.watch((value) => console.log(value));
93
310
  *
94
- * @param {Function} fn - The callback function to invoke on value change.
95
- * @returns {Function} A function to unsubscribe the watcher.
311
+ * @example
312
+ * // Stop watching
313
+ * unsubscribe(); // Returns true if watcher was removed
314
+ *
315
+ * @example
316
+ * // Multiple watchers
317
+ * const unsub1 = signal.watch((v) => console.log("Watcher 1:", v));
318
+ * const unsub2 = signal.watch((v) => console.log("Watcher 2:", v));
319
+ * signal.value = "test"; // Both watchers are called
96
320
  */
97
321
  watch(fn) {
98
322
  this._watchers.add(fn);
99
323
  return () => this._watchers.delete(fn);
100
324
  }
325
+
326
+ /**
327
+ * Notifies all registered watchers of a value change using microtask scheduling.
328
+ * Uses a pending flag to batch multiple synchronous updates into a single notification.
329
+ * All watcher callbacks receive the current value when executed.
330
+ *
331
+ * @private
332
+ * @returns {void}
333
+ */
334
+ _notify() {
335
+ if (this._pending) return;
336
+ this._pending = true;
337
+ queueMicrotask(() => {
338
+ /** @type {(fn: (value: T) => void) => void} */
339
+ this._watchers.forEach(fn => fn(this._value));
340
+ this._pending = false;
341
+ });
342
+ }
101
343
  }
102
344
 
345
+ // ============================================================================
346
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
347
+ // ============================================================================
348
+
349
+ /**
350
+ * @template T
351
+ * @callback EventHandler
352
+ * @param {...T} args - Event arguments
353
+ * @returns {void|Promise<void>}
354
+ */
355
+
356
+ /**
357
+ * @callback EventUnsubscribe
358
+ * @returns {void}
359
+ */
360
+
361
+ /**
362
+ * @typedef {`${string}:${string}`} EventName
363
+ * Event names follow the format 'namespace:action' (e.g., 'user:login', 'cart:update')
364
+ */
365
+
366
+ /**
367
+ * @typedef {Object} EmitterLike
368
+ * @property {function(string, EventHandler<unknown>): EventUnsubscribe} on - Subscribe to an event
369
+ * @property {function(string, EventHandler<unknown>=): void} off - Unsubscribe from an event
370
+ * @property {function(string, ...unknown): void} emit - Emit an event
371
+ */
372
+
103
373
  /**
104
- * 🎙️ Emitter: Robust inter-component communication with event bubbling.
374
+ * @class 📡 Emitter
375
+ * @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
376
+ * Components can emit events and listen for events from other components, facilitating loose coupling
377
+ * and reactive updates across the application.
378
+ * Events are handled synchronously in the order they were registered, with proper cleanup
379
+ * of unsubscribed handlers.
380
+ *
381
+ * Event names should follow the format 'namespace:action' for consistency and organization.
382
+ *
383
+ * @example
384
+ * // Basic usage
385
+ * const emitter = new Emitter();
386
+ * emitter.on('user:login', (user) => console.log(`User logged in: ${user.name}`));
387
+ * emitter.emit('user:login', { name: 'John' }); // Logs: "User logged in: John"
388
+ *
389
+ * @example
390
+ * // With unsubscribe
391
+ * const unsub = emitter.on('cart:update', (items) => {
392
+ * console.log(`Cart has ${items.length} items`);
393
+ * });
394
+ * emitter.emit('cart:update', [{ id: 1, name: 'Book' }]); // Logs: "Cart has 1 items"
395
+ * unsub(); // Stop listening
396
+ * emitter.emit('cart:update', []); // No log output
105
397
  *
106
- * Implements a basic publish-subscribe pattern for event handling,
107
- * allowing components to communicate through custom events.
398
+ * @example
399
+ * // Multiple arguments
400
+ * emitter.on('order:placed', (orderId, amount, currency) => {
401
+ * console.log(`Order ${orderId}: ${amount} ${currency}`);
402
+ * });
403
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
404
+ *
405
+ * @example
406
+ * // Common event patterns
407
+ * // Lifecycle events
408
+ * emitter.on('component:mount', (component) => {});
409
+ * emitter.on('component:unmount', (component) => {});
410
+ * // State events
411
+ * emitter.on('state:change', (newState, oldState) => {});
412
+ * // Navigation events
413
+ * emitter.on('router:navigate', (to, from) => {});
414
+ *
415
+ * @implements {EmitterLike}
108
416
  */
109
417
  class Emitter {
110
418
  /**
111
419
  * Creates a new Emitter instance.
420
+ *
421
+ * @public
422
+ *
423
+ * @example
424
+ * const emitter = new Emitter();
112
425
  */
113
426
  constructor() {
114
- /** @type {Object.<string, Function[]>} */
115
- this.events = {};
427
+ /**
428
+ * Map of event names to their registered handler functions
429
+ * @private
430
+ * @type {Map<string, Set<EventHandler<unknown>>>}
431
+ */
432
+ this._events = new Map();
116
433
  }
117
434
 
118
435
  /**
119
- * Registers an event handler for the specified event.
436
+ * Registers an event handler for the specified event name.
437
+ * The handler will be called with the event data when the event is emitted.
438
+ * Event names should follow the format 'namespace:action' for consistency.
439
+ *
440
+ * @public
441
+ * @template T
442
+ * @param {string} event - The name of the event to listen for (e.g., 'user:login').
443
+ * @param {EventHandler<T>} handler - The callback function to invoke when the event occurs.
444
+ * @returns {EventUnsubscribe} A function to unsubscribe the event handler.
445
+ *
446
+ * @example
447
+ * // Basic subscription
448
+ * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
120
449
  *
121
- * @param {string} event - The name of the event.
122
- * @param {Function} handler - The function to call when the event is emitted.
450
+ * @example
451
+ * // Typed handler
452
+ * emitter.on('user:update', (/** @type {{id: number, name: string}} *\/ user) => {
453
+ * console.log(`User ${user.id}: ${user.name}`);
454
+ * });
455
+ *
456
+ * @example
457
+ * // Cleanup
458
+ * unsubscribe(); // Stops listening for the event
123
459
  */
124
460
  on(event, handler) {
125
- (this.events[event] || (this.events[event] = [])).push(handler);
461
+ if (!this._events.has(event)) this._events.set(event, new Set());
462
+ this._events.get(event).add(handler);
463
+ return () => this.off(event, handler);
126
464
  }
127
465
 
128
466
  /**
129
- * Removes a previously registered event handler.
467
+ * Removes an event handler for the specified event name.
468
+ * If no handler is provided, all handlers for the event are removed.
469
+ * Automatically cleans up empty event sets to prevent memory leaks.
470
+ *
471
+ * @public
472
+ * @template T
473
+ * @param {string} event - The name of the event to remove handlers from.
474
+ * @param {EventHandler<T>} [handler] - The specific handler function to remove.
475
+ * @returns {void}
130
476
  *
131
- * @param {string} event - The name of the event.
132
- * @param {Function} handler - The handler function to remove.
477
+ * @example
478
+ * // Remove a specific handler
479
+ * const loginHandler = (user) => console.log(user);
480
+ * emitter.on('user:login', loginHandler);
481
+ * emitter.off('user:login', loginHandler);
482
+ *
483
+ * @example
484
+ * // Remove all handlers for an event
485
+ * emitter.off('user:login');
133
486
  */
134
487
  off(event, handler) {
135
- if (this.events[event]) {
136
- this.events[event] = this.events[event].filter(h => h !== handler);
488
+ if (!this._events.has(event)) return;
489
+ if (handler) {
490
+ const handlers = this._events.get(event);
491
+ handlers.delete(handler);
492
+ // Remove the event if there are no handlers left
493
+ if (handlers.size === 0) this._events.delete(event);
494
+ } else {
495
+ this._events.delete(event);
137
496
  }
138
497
  }
139
498
 
140
499
  /**
141
- * Emits an event, invoking all handlers registered for that event.
500
+ * Emits an event with the specified data to all registered handlers.
501
+ * Handlers are called synchronously in the order they were registered.
502
+ * If no handlers are registered for the event, the emission is silently ignored.
503
+ *
504
+ * @public
505
+ * @template T
506
+ * @param {string} event - The name of the event to emit.
507
+ * @param {...T} args - Optional arguments to pass to the event handlers.
508
+ * @returns {void}
509
+ *
510
+ * @example
511
+ * // Emit an event with data
512
+ * emitter.emit('user:login', { name: 'John', role: 'admin' });
513
+ *
514
+ * @example
515
+ * // Emit an event with multiple arguments
516
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
142
517
  *
143
- * @param {string} event - The event name.
144
- * @param {...*} args - Additional arguments to pass to the event handlers.
518
+ * @example
519
+ * // Emit without data
520
+ * emitter.emit('app:ready');
145
521
  */
146
522
  emit(event, ...args) {
147
- (this.events[event] || []).forEach(handler => handler(...args));
523
+ if (!this._events.has(event)) return;
524
+ this._events.get(event).forEach(handler => handler(...args));
148
525
  }
149
526
  }
150
527
 
528
+ // ============================================================================
529
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
530
+ // ============================================================================
531
+
532
+ /**
533
+ * @typedef {Object} PatchOptions
534
+ * @property {boolean} [preserveStyles=true]
535
+ * Whether to preserve style elements with data-e-style attribute
536
+ * @property {boolean} [preserveInstances=true]
537
+ * Whether to preserve elements with _eleva_instance property
538
+ */
539
+
540
+ /**
541
+ * @typedef {Map<string, Node>} KeyMap
542
+ * Map of key attribute values to their corresponding DOM nodes
543
+ */
544
+
545
+ /**
546
+ * @typedef {'ELEMENT_NODE'|'TEXT_NODE'|'COMMENT_NODE'|'DOCUMENT_FRAGMENT_NODE'} NodeTypeName
547
+ * Common DOM node type names
548
+ */
549
+
151
550
  /**
152
- * 🎨 Renderer: Handles DOM patching, diffing, and attribute updates.
551
+ * @class 🎨 Renderer
552
+ * @classdesc A high-performance DOM renderer that implements an optimized direct DOM diffing algorithm.
153
553
  *
154
- * Provides methods for efficient DOM updates by diffing the new and old DOM structures
155
- * and applying only the necessary changes.
554
+ * Key features:
555
+ * - Single-pass diffing algorithm for efficient DOM updates
556
+ * - Key-based node reconciliation for optimal performance
557
+ * - Intelligent attribute handling for ARIA, data attributes, and boolean properties
558
+ * - Preservation of special Eleva-managed instances and style elements
559
+ * - Memory-efficient with reusable temporary containers
560
+ *
561
+ * The renderer is designed to minimize DOM operations while maintaining
562
+ * exact attribute synchronization and proper node identity preservation.
563
+ * It's particularly optimized for frequent updates and complex DOM structures.
564
+ *
565
+ * @example
566
+ * // Basic usage
567
+ * const renderer = new Renderer();
568
+ * const container = document.getElementById("app");
569
+ * const newHtml = "<div>Updated content</div>";
570
+ * renderer.patchDOM(container, newHtml);
571
+ *
572
+ * @example
573
+ * // With keyed elements for optimal list updates
574
+ * const listHtml = `
575
+ * <ul>
576
+ * <li key="item-1">First</li>
577
+ * <li key="item-2">Second</li>
578
+ * <li key="item-3">Third</li>
579
+ * </ul>
580
+ * `;
581
+ * renderer.patchDOM(container, listHtml);
582
+ *
583
+ * @example
584
+ * // The renderer preserves Eleva-managed elements
585
+ * // Elements with _eleva_instance are not replaced during diffing
586
+ * // Style elements with data-e-style are preserved
156
587
  */
157
588
  class Renderer {
158
589
  /**
159
- * Patches the DOM of a container element with new HTML content.
590
+ * Creates a new Renderer instance.
591
+ *
592
+ * @public
160
593
  *
594
+ * @example
595
+ * const renderer = new Renderer();
596
+ */
597
+ constructor() {
598
+ /**
599
+ * A temporary container to hold the new HTML content while diffing.
600
+ * Reused across patch operations to minimize memory allocation.
601
+ * @private
602
+ * @type {HTMLDivElement}
603
+ */
604
+ this._tempContainer = document.createElement("div");
605
+ }
606
+
607
+ /**
608
+ * Patches the DOM of the given container with the provided HTML string.
609
+ * Uses an optimized diffing algorithm to minimize DOM operations.
610
+ *
611
+ * @public
161
612
  * @param {HTMLElement} container - The container element to patch.
162
- * @param {string} newHtml - The new HTML content to apply.
613
+ * @param {string} newHtml - The new HTML string.
614
+ * @returns {void}
615
+ * @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
616
+ * @throws {Error} If DOM patching fails.
617
+ *
618
+ * @example
619
+ * // Update container content
620
+ * renderer.patchDOM(container, '<div class="updated">New content</div>');
621
+ *
622
+ * @example
623
+ * // Update list with keys for optimal diffing
624
+ * const items = ['a', 'b', 'c'];
625
+ * const html = items.map(item =>
626
+ * `<li key="${item}">${item}</li>`
627
+ * ).join('');
628
+ * renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
163
629
  */
164
630
  patchDOM(container, newHtml) {
165
- const tempContainer = document.createElement("div");
166
- tempContainer.innerHTML = newHtml;
167
- this.diff(container, tempContainer);
631
+ if (!(container instanceof HTMLElement)) {
632
+ throw new TypeError("Container must be an HTMLElement");
633
+ }
634
+ if (typeof newHtml !== "string") {
635
+ throw new TypeError("newHtml must be a string");
636
+ }
637
+ try {
638
+ this._tempContainer.innerHTML = newHtml;
639
+ this._diff(container, this._tempContainer);
640
+ } catch (error) {
641
+ throw new Error(`Failed to patch DOM: ${error.message}`);
642
+ }
168
643
  }
169
644
 
170
645
  /**
171
- * Diffs two DOM trees (old and new) and applies updates to the old DOM.
646
+ * Performs a diff between two DOM nodes and patches the old node to match the new node.
172
647
  *
648
+ * @private
173
649
  * @param {HTMLElement} oldParent - The original DOM element.
174
650
  * @param {HTMLElement} newParent - The new DOM element.
651
+ * @returns {void}
175
652
  */
176
- diff(oldParent, newParent) {
177
- const oldNodes = Array.from(oldParent.childNodes);
178
- const newNodes = Array.from(newParent.childNodes);
179
- const max = Math.max(oldNodes.length, newNodes.length);
180
- for (let i = 0; i < max; i++) {
181
- const oldNode = oldNodes[i];
182
- const newNode = newNodes[i];
183
-
184
- // Append new nodes that don't exist in the old tree.
185
- if (!oldNode && newNode) {
186
- oldParent.appendChild(newNode.cloneNode(true));
187
- continue;
188
- }
189
- // Remove old nodes not present in the new tree.
190
- if (oldNode && !newNode) {
191
- oldParent.removeChild(oldNode);
192
- continue;
193
- }
194
-
195
- // For element nodes, compare keys if available.
196
- if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
197
- const oldKey = oldNode.getAttribute("key");
198
- const newKey = newNode.getAttribute("key");
199
- if (oldKey || newKey) {
200
- if (oldKey !== newKey) {
201
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
202
- continue;
203
- }
653
+ _diff(oldParent, newParent) {
654
+ if (oldParent === newParent || oldParent.isEqualNode?.(newParent)) return;
655
+ const oldChildren = Array.from(oldParent.childNodes);
656
+ const newChildren = Array.from(newParent.childNodes);
657
+ let oldStartIdx = 0,
658
+ newStartIdx = 0;
659
+ let oldEndIdx = oldChildren.length - 1;
660
+ let newEndIdx = newChildren.length - 1;
661
+ let oldKeyMap = null;
662
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
663
+ let oldStartNode = oldChildren[oldStartIdx];
664
+ let newStartNode = newChildren[newStartIdx];
665
+ if (!oldStartNode) {
666
+ oldStartNode = oldChildren[++oldStartIdx];
667
+ } else if (this._isSameNode(oldStartNode, newStartNode)) {
668
+ this._patchNode(oldStartNode, newStartNode);
669
+ oldStartIdx++;
670
+ newStartIdx++;
671
+ } else {
672
+ if (!oldKeyMap) {
673
+ oldKeyMap = this._createKeyMap(oldChildren, oldStartIdx, oldEndIdx);
204
674
  }
205
- }
206
-
207
- // Replace nodes if types or tag names differ.
208
- if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
209
- oldParent.replaceChild(newNode.cloneNode(true), oldNode);
210
- continue;
211
- }
212
- // For text nodes, update content if different.
213
- if (oldNode.nodeType === Node.TEXT_NODE) {
214
- if (oldNode.nodeValue !== newNode.nodeValue) {
215
- oldNode.nodeValue = newNode.nodeValue;
675
+ const key = this._getNodeKey(newStartNode);
676
+ const oldNodeToMove = key ? oldKeyMap.get(key) : null;
677
+ if (oldNodeToMove) {
678
+ this._patchNode(oldNodeToMove, newStartNode);
679
+ oldParent.insertBefore(oldNodeToMove, oldStartNode);
680
+ oldChildren[oldChildren.indexOf(oldNodeToMove)] = null;
681
+ } else {
682
+ oldParent.insertBefore(newStartNode.cloneNode(true), oldStartNode);
216
683
  }
217
- continue;
684
+ newStartIdx++;
218
685
  }
219
- // For element nodes, update attributes and then diff children.
220
- if (oldNode.nodeType === Node.ELEMENT_NODE) {
221
- this.updateAttributes(oldNode, newNode);
222
- this.diff(oldNode, newNode);
686
+ }
687
+ if (oldStartIdx > oldEndIdx) {
688
+ const refNode = newChildren[newEndIdx + 1] ? oldChildren[oldStartIdx] : null;
689
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
690
+ if (newChildren[i]) oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
691
+ }
692
+ } else if (newStartIdx > newEndIdx) {
693
+ for (let i = oldStartIdx; i <= oldEndIdx; i++) {
694
+ if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
223
695
  }
224
696
  }
225
697
  }
226
698
 
227
699
  /**
228
- * Updates the attributes of an element to match those of a new element.
700
+ * Patches a single node.
229
701
  *
230
- * @param {HTMLElement} oldEl - The element to update.
231
- * @param {HTMLElement} newEl - The element providing the updated attributes.
702
+ * @private
703
+ * @param {Node} oldNode - The original DOM node.
704
+ * @param {Node} newNode - The new DOM node.
705
+ * @returns {void}
232
706
  */
233
- updateAttributes(oldEl, newEl) {
234
- const attributeToPropertyMap = {
235
- value: "value",
236
- checked: "checked",
237
- selected: "selected",
238
- disabled: "disabled"
239
- };
707
+ _patchNode(oldNode, newNode) {
708
+ if (oldNode?._eleva_instance) return;
709
+ if (!this._isSameNode(oldNode, newNode)) {
710
+ oldNode.replaceWith(newNode.cloneNode(true));
711
+ return;
712
+ }
713
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
714
+ this._updateAttributes(oldNode, newNode);
715
+ this._diff(oldNode, newNode);
716
+ } else if (oldNode.nodeType === Node.TEXT_NODE && oldNode.nodeValue !== newNode.nodeValue) {
717
+ oldNode.nodeValue = newNode.nodeValue;
718
+ }
719
+ }
240
720
 
241
- // Remove old attributes that no longer exist.
242
- Array.from(oldEl.attributes).forEach(attr => {
243
- if (attr.name.startsWith("@")) return;
244
- if (!newEl.hasAttribute(attr.name)) {
245
- oldEl.removeAttribute(attr.name);
246
- }
247
- });
248
- // Add or update attributes from newEl.
249
- Array.from(newEl.attributes).forEach(attr => {
250
- if (attr.name.startsWith("@")) return;
251
- if (oldEl.getAttribute(attr.name) !== attr.value) {
252
- oldEl.setAttribute(attr.name, attr.value);
253
- if (attributeToPropertyMap[attr.name]) {
254
- oldEl[attributeToPropertyMap[attr.name]] = attr.value;
255
- } else if (attr.name in oldEl) {
256
- oldEl[attr.name] = attr.value;
257
- }
721
+ /**
722
+ * Removes a node from its parent.
723
+ *
724
+ * @private
725
+ * @param {HTMLElement} parent - The parent element containing the node to remove.
726
+ * @param {Node} node - The node to remove.
727
+ * @returns {void}
728
+ */
729
+ _removeNode(parent, node) {
730
+ if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
731
+ parent.removeChild(node);
732
+ }
733
+
734
+ /**
735
+ * Updates the attributes of an element to match a new element's attributes.
736
+ *
737
+ * @private
738
+ * @param {HTMLElement} oldEl - The original element to update.
739
+ * @param {HTMLElement} newEl - The new element to update.
740
+ * @returns {void}
741
+ */
742
+ _updateAttributes(oldEl, newEl) {
743
+ const oldAttrs = oldEl.attributes;
744
+ const newAttrs = newEl.attributes;
745
+
746
+ // Process new attributes
747
+ for (let i = 0; i < newAttrs.length; i++) {
748
+ const {
749
+ name,
750
+ value
751
+ } = newAttrs[i];
752
+
753
+ // Skip event attributes (handled by event system)
754
+ if (name.startsWith("@")) continue;
755
+
756
+ // Skip if attribute hasn't changed
757
+ if (oldEl.getAttribute(name) === value) continue;
758
+
759
+ // Basic attribute setting
760
+ oldEl.setAttribute(name, value);
761
+ }
762
+
763
+ // Remove old attributes that are no longer present
764
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
765
+ const name = oldAttrs[i].name;
766
+ if (!newEl.hasAttribute(name)) {
767
+ oldEl.removeAttribute(name);
258
768
  }
259
- });
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Determines if two nodes are the same based on their type, name, and key attributes.
774
+ *
775
+ * @private
776
+ * @param {Node} oldNode - The first node to compare.
777
+ * @param {Node} newNode - The second node to compare.
778
+ * @returns {boolean} True if the nodes are considered the same, false otherwise.
779
+ */
780
+ _isSameNode(oldNode, newNode) {
781
+ if (!oldNode || !newNode) return false;
782
+ const oldKey = oldNode.nodeType === Node.ELEMENT_NODE ? oldNode.getAttribute("key") : null;
783
+ const newKey = newNode.nodeType === Node.ELEMENT_NODE ? newNode.getAttribute("key") : null;
784
+ if (oldKey && newKey) return oldKey === newKey;
785
+ return !oldKey && !newKey && oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName;
786
+ }
787
+
788
+ /**
789
+ * Creates a key map for the children of a parent node.
790
+ * Used for efficient O(1) lookup of keyed elements during diffing.
791
+ *
792
+ * @private
793
+ * @param {Array<ChildNode>} children - The children of the parent node.
794
+ * @param {number} start - The start index of the children.
795
+ * @param {number} end - The end index of the children.
796
+ * @returns {KeyMap} A key map for the children.
797
+ */
798
+ _createKeyMap(children, start, end) {
799
+ const map = new Map();
800
+ for (let i = start; i <= end; i++) {
801
+ const child = children[i];
802
+ const key = this._getNodeKey(child);
803
+ if (key) map.set(key, child);
804
+ }
805
+ return map;
806
+ }
807
+
808
+ /**
809
+ * Extracts the key attribute from a node if it exists.
810
+ *
811
+ * @private
812
+ * @param {Node} node - The node to extract the key from.
813
+ * @returns {string|null} The key attribute value or null if not found.
814
+ */
815
+ _getNodeKey(node) {
816
+ return node?.nodeType === Node.ELEMENT_NODE ? node.getAttribute("key") : null;
260
817
  }
261
818
  }
262
819
 
820
+ // ============================================================================
821
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
822
+ // ============================================================================
823
+
824
+ // -----------------------------------------------------------------------------
825
+ // Configuration Types
826
+ // -----------------------------------------------------------------------------
827
+
828
+ /**
829
+ * @typedef {Object} ElevaConfig
830
+ * @property {boolean} [debug=false]
831
+ * Enable debug mode for verbose logging
832
+ * @property {string} [prefix='e']
833
+ * Prefix for component style scoping
834
+ * @property {boolean} [async=true]
835
+ * Enable async component setup
836
+ */
837
+
838
+ // -----------------------------------------------------------------------------
839
+ // Component Types
840
+ // -----------------------------------------------------------------------------
841
+
842
+ /**
843
+ * @typedef {Object} ComponentDefinition
844
+ * @property {SetupFunction} [setup]
845
+ * Optional setup function that initializes the component's state and returns reactive data
846
+ * @property {TemplateFunction|string} template
847
+ * Required function or string that defines the component's HTML structure
848
+ * @property {StyleFunction|string} [style]
849
+ * Optional function or string that provides component-scoped CSS styles
850
+ * @property {ChildrenMap} [children]
851
+ * Optional object defining nested child components
852
+ */
853
+
854
+ /**
855
+ * @callback SetupFunction
856
+ * @param {ComponentContext} ctx - The component context with props, emitter, and signal factory
857
+ * @returns {SetupResult|Promise<SetupResult>} Reactive data and lifecycle hooks
858
+ */
859
+
860
+ /**
861
+ * @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
862
+ * Data returned from setup function, may include lifecycle hooks
863
+ */
864
+
865
+ /**
866
+ * @callback TemplateFunction
867
+ * @param {ComponentContext} ctx - The component context
868
+ * @returns {string|Promise<string>} HTML template string
869
+ */
870
+
871
+ /**
872
+ * @callback StyleFunction
873
+ * @param {ComponentContext} ctx - The component context
874
+ * @returns {string} CSS styles string
875
+ */
876
+
877
+ /**
878
+ * @typedef {Record<string, ComponentDefinition|string>} ChildrenMap
879
+ * Map of CSS selectors to component definitions or registered component names
880
+ */
881
+
882
+ // -----------------------------------------------------------------------------
883
+ // Context Types
884
+ // -----------------------------------------------------------------------------
885
+
886
+ /**
887
+ * @typedef {Object} ComponentContext
888
+ * @property {ComponentProps} props
889
+ * Component properties passed during mounting
890
+ * @property {Emitter} emitter
891
+ * Event emitter instance for component event handling
892
+ * @property {SignalFactory} signal
893
+ * Factory function to create reactive Signal instances
894
+ */
895
+
896
+ /**
897
+ * @typedef {Record<string, unknown>} ComponentProps
898
+ * Properties passed to a component during mounting
899
+ */
900
+
901
+ /**
902
+ * @callback SignalFactory
903
+ * @template T
904
+ * @param {T} initialValue - The initial value for the signal
905
+ * @returns {Signal<T>} A new Signal instance
906
+ */
907
+
908
+ // -----------------------------------------------------------------------------
909
+ // Lifecycle Hook Types
910
+ // -----------------------------------------------------------------------------
911
+
912
+ /**
913
+ * @typedef {Object} LifecycleHooks
914
+ * @property {LifecycleHook} [onBeforeMount]
915
+ * Hook called before component mounting
916
+ * @property {LifecycleHook} [onMount]
917
+ * Hook called after component mounting
918
+ * @property {LifecycleHook} [onBeforeUpdate]
919
+ * Hook called before component update
920
+ * @property {LifecycleHook} [onUpdate]
921
+ * Hook called after component update
922
+ * @property {UnmountHook} [onUnmount]
923
+ * Hook called during component unmounting
924
+ */
925
+
926
+ /**
927
+ * @callback LifecycleHook
928
+ * @param {LifecycleHookContext} ctx - Context with container and component data
929
+ * @returns {void|Promise<void>}
930
+ */
931
+
932
+ /**
933
+ * @callback UnmountHook
934
+ * @param {UnmountHookContext} ctx - Context with cleanup resources
935
+ * @returns {void|Promise<void>}
936
+ */
937
+
938
+ /**
939
+ * @typedef {Object} LifecycleHookContext
940
+ * @property {HTMLElement} container
941
+ * The DOM element where the component is mounted
942
+ * @property {ComponentContext & SetupResult} context
943
+ * The component's reactive state and context data
944
+ */
945
+
946
+ /**
947
+ * @typedef {Object} UnmountHookContext
948
+ * @property {HTMLElement} container
949
+ * The DOM element where the component is mounted
950
+ * @property {ComponentContext & SetupResult} context
951
+ * The component's reactive state and context data
952
+ * @property {CleanupResources} cleanup
953
+ * Object containing cleanup functions and instances
954
+ */
955
+
263
956
  /**
264
- * 🧩 Eleva Core: Signal-based component runtime framework with lifecycle, scoped styles, and plugins.
957
+ * @typedef {Object} CleanupResources
958
+ * @property {Array<UnsubscribeFunction>} watchers
959
+ * Signal watcher cleanup functions
960
+ * @property {Array<UnsubscribeFunction>} listeners
961
+ * Event listener cleanup functions
962
+ * @property {Array<MountResult>} children
963
+ * Child component instances
964
+ */
965
+
966
+ // -----------------------------------------------------------------------------
967
+ // Mount Result Types
968
+ // -----------------------------------------------------------------------------
969
+
970
+ /**
971
+ * @typedef {Object} MountResult
972
+ * @property {HTMLElement} container
973
+ * The DOM element where the component is mounted
974
+ * @property {ComponentContext & SetupResult} data
975
+ * The component's reactive state and context data
976
+ * @property {UnmountFunction} unmount
977
+ * Function to clean up and unmount the component
978
+ */
979
+
980
+ /**
981
+ * @callback UnmountFunction
982
+ * @returns {Promise<void>}
983
+ */
984
+
985
+ /**
986
+ * @callback UnsubscribeFunction
987
+ * @returns {void|boolean}
988
+ */
989
+
990
+ // -----------------------------------------------------------------------------
991
+ // Plugin Types
992
+ // -----------------------------------------------------------------------------
993
+
994
+ /**
995
+ * @typedef {Object} ElevaPlugin
996
+ * @property {PluginInstallFunction} install
997
+ * Function that installs the plugin into the Eleva instance
998
+ * @property {string} name
999
+ * Unique identifier name for the plugin
1000
+ * @property {PluginUninstallFunction} [uninstall]
1001
+ * Optional function to uninstall the plugin
1002
+ */
1003
+
1004
+ /**
1005
+ * @callback PluginInstallFunction
1006
+ * @param {Eleva} eleva - The Eleva instance
1007
+ * @param {PluginOptions} options - Plugin configuration options
1008
+ * @returns {void|Eleva|unknown} Optionally returns the Eleva instance or plugin result
1009
+ */
1010
+
1011
+ /**
1012
+ * @callback PluginUninstallFunction
1013
+ * @param {Eleva} eleva - The Eleva instance
1014
+ * @returns {void}
1015
+ */
1016
+
1017
+ /**
1018
+ * @typedef {Record<string, unknown>} PluginOptions
1019
+ * Configuration options passed to a plugin during installation
1020
+ */
1021
+
1022
+ // -----------------------------------------------------------------------------
1023
+ // Event Types
1024
+ // -----------------------------------------------------------------------------
1025
+
1026
+ /**
1027
+ * @callback EventHandler
1028
+ * @param {Event} event - The DOM event object
1029
+ * @returns {void}
1030
+ */
1031
+
1032
+ /**
1033
+ * @typedef {'click'|'submit'|'input'|'change'|'focus'|'blur'|'keydown'|'keyup'|'keypress'|'mouseenter'|'mouseleave'|'mouseover'|'mouseout'|'mousedown'|'mouseup'|'touchstart'|'touchend'|'touchmove'|'scroll'|'resize'|'load'|'error'|string} DOMEventName
1034
+ * Common DOM event names (prefixed with @ in templates)
1035
+ */
1036
+
1037
+ /**
1038
+ * @class 🧩 Eleva
1039
+ * @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
1040
+ * scoped styles, and plugin support. Eleva manages component registration, plugin integration,
1041
+ * event handling, and DOM rendering with a focus on performance and developer experience.
265
1042
  *
266
- * The Eleva class is the core of the framework. It manages component registration,
267
- * plugin integration, lifecycle hooks, event handling, and DOM rendering.
1043
+ * @example
1044
+ * // Basic component creation and mounting
1045
+ * const app = new Eleva("myApp");
1046
+ * app.component("myComponent", {
1047
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
1048
+ * template: (ctx) => `<div>Hello ${ctx.props.name}</div>`
1049
+ * });
1050
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
1051
+ *
1052
+ * @example
1053
+ * // Using lifecycle hooks
1054
+ * app.component("lifecycleDemo", {
1055
+ * setup: () => {
1056
+ * return {
1057
+ * onMount: ({ container, context }) => {
1058
+ * console.log('Component mounted!');
1059
+ * }
1060
+ * };
1061
+ * },
1062
+ * template: `<div>Lifecycle Demo</div>`
1063
+ * });
268
1064
  */
269
1065
  class Eleva {
270
1066
  /**
271
- * Creates a new Eleva instance.
1067
+ * Creates a new Eleva instance with the specified name and configuration.
1068
+ *
1069
+ * @public
1070
+ * @param {string} name - The unique identifier name for this Eleva instance.
1071
+ * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
1072
+ * May include framework-wide settings and default behaviors.
1073
+ * @throws {Error} If the name is not provided or is not a string.
1074
+ * @returns {Eleva} A new Eleva instance.
1075
+ *
1076
+ * @example
1077
+ * const app = new Eleva("myApp");
1078
+ * app.component("myComponent", {
1079
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
1080
+ * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
1081
+ * });
1082
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
272
1083
  *
273
- * @param {string} name - The name of the Eleva instance.
274
- * @param {object} [config={}] - Optional configuration for the instance.
275
1084
  */
276
1085
  constructor(name, config = {}) {
1086
+ /** @public {string} The unique identifier name for this Eleva instance */
277
1087
  this.name = name;
1088
+ /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */
278
1089
  this.config = config;
279
- this._components = {};
280
- this._plugins = [];
281
- this._lifecycleHooks = ["onBeforeMount", "onMount", "onBeforeUpdate", "onUpdate", "onUnmount"];
282
- this._isMounted = false;
1090
+ /** @public {Emitter} Instance of the event emitter for handling component events */
283
1091
  this.emitter = new Emitter();
1092
+ /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
1093
+ this.signal = Signal;
1094
+ /** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */
1095
+ this.templateEngine = TemplateEngine;
1096
+ /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
284
1097
  this.renderer = new Renderer();
1098
+
1099
+ /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */
1100
+ this._components = new Map();
1101
+ /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
1102
+ this._plugins = new Map();
1103
+ /** @private {number} Counter for generating unique component IDs */
1104
+ this._componentCounter = 0;
285
1105
  }
286
1106
 
287
1107
  /**
288
1108
  * Integrates a plugin with the Eleva framework.
1109
+ * The plugin's install function will be called with the Eleva instance and provided options.
1110
+ * After installation, the plugin will be available for use by components.
1111
+ *
1112
+ * Note: Plugins that wrap core methods (e.g., mount) must be uninstalled in reverse order
1113
+ * of installation (LIFO - Last In, First Out) to avoid conflicts.
1114
+ *
1115
+ * @public
1116
+ * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
1117
+ * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
1118
+ * @returns {Eleva} The Eleva instance (for method chaining).
1119
+ * @example
1120
+ * app.use(myPlugin, { option1: "value1" });
289
1121
  *
290
- * @param {object} [plugin] - The plugin object which should have an install function.
291
- * @param {object} [options={}] - Optional options to pass to the plugin.
292
- * @returns {Eleva} The Eleva instance (for chaining).
1122
+ * @example
1123
+ * // Correct uninstall order (LIFO)
1124
+ * app.use(PluginA);
1125
+ * app.use(PluginB);
1126
+ * // Uninstall in reverse order:
1127
+ * PluginB.uninstall(app);
1128
+ * PluginA.uninstall(app);
293
1129
  */
294
1130
  use(plugin, options = {}) {
295
- if (typeof plugin.install === "function") {
296
- plugin.install(this, options);
297
- }
298
- this._plugins.push(plugin);
299
- return this;
1131
+ this._plugins.set(plugin.name, plugin);
1132
+ const result = plugin.install(this, options);
1133
+ return result !== undefined ? result : this;
300
1134
  }
301
1135
 
302
1136
  /**
303
- * Registers a component with the Eleva instance.
1137
+ * Registers a new component with the Eleva instance.
1138
+ * The component will be available for mounting using its registered name.
304
1139
  *
305
- * @param {string} name - The name of the component.
306
- * @param {object} definition - The component definition including setup, template, style, and children.
307
- * @returns {Eleva} The Eleva instance (for chaining).
1140
+ * @public
1141
+ * @param {string} name - The unique name of the component to register.
1142
+ * @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
1143
+ * @returns {Eleva} The Eleva instance (for method chaining).
1144
+ * @throws {Error} If the component name is already registered.
1145
+ * @example
1146
+ * app.component("myButton", {
1147
+ * template: (ctx) => `<button>${ctx.props.text}</button>`,
1148
+ * style: `button { color: blue; }`
1149
+ * });
308
1150
  */
309
1151
  component(name, definition) {
310
- this._components[name] = definition;
1152
+ /** @type {Map<string, ComponentDefinition>} */
1153
+ this._components.set(name, definition);
311
1154
  return this;
312
1155
  }
313
1156
 
314
1157
  /**
315
1158
  * Mounts a registered component to a DOM element.
1159
+ * This will initialize the component, set up its reactive state, and render it to the DOM.
316
1160
  *
317
- * @param {string|HTMLElement} selectorOrElement - A CSS selector string or DOM element where the component will be mounted.
318
- * @param {string} compName - The name of the component to mount.
319
- * @param {object} [props={}] - Optional properties to pass to the component.
320
- * @returns {object|Promise<object>} An object representing the mounted component instance, or a Promise that resolves to it for asynchronous setups.
321
- * @throws Will throw an error if the container or component is not found.
1161
+ * @public
1162
+ * @param {HTMLElement} container - The DOM element where the component will be mounted.
1163
+ * @param {string|ComponentDefinition} compName - The name of the registered component or a direct component definition.
1164
+ * @param {Object<string, unknown>} [props={}] - Optional properties to pass to the component.
1165
+ * @returns {Promise<MountResult>}
1166
+ * A Promise that resolves to an object containing:
1167
+ * - container: The mounted component's container element
1168
+ * - data: The component's reactive state and context
1169
+ * - unmount: Function to clean up and unmount the component
1170
+ * @throws {Error} If the container is not found, or component is not registered.
1171
+ * @example
1172
+ * const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
1173
+ * // Later...
1174
+ * instance.unmount();
322
1175
  */
323
- mount(selectorOrElement, compName, props = {}) {
324
- const container = typeof selectorOrElement === "string" ? document.querySelector(selectorOrElement) : selectorOrElement;
325
- if (!container) throw new Error(`Container not found: ${selectorOrElement}`);
326
- const definition = this._components[compName];
1176
+ async mount(container, compName, props = {}) {
1177
+ if (!container) throw new Error(`Container not found: ${container}`);
1178
+ if (container._eleva_instance) return container._eleva_instance;
1179
+
1180
+ /** @type {ComponentDefinition} */
1181
+ const definition = typeof compName === "string" ? this._components.get(compName) : compName;
327
1182
  if (!definition) throw new Error(`Component "${compName}" not registered.`);
1183
+
1184
+ /** @type {string} */
1185
+ const compId = `c${++this._componentCounter}`;
1186
+
1187
+ /**
1188
+ * Destructure the component definition to access core functionality.
1189
+ * - setup: Optional function for component initialization and state management
1190
+ * - template: Required function or string that returns the component's HTML structure
1191
+ * - style: Optional function or string for component-scoped CSS styles
1192
+ * - children: Optional object defining nested child components
1193
+ */
328
1194
  const {
329
1195
  setup,
330
1196
  template,
331
1197
  style,
332
1198
  children
333
1199
  } = definition;
1200
+
1201
+ /** @type {ComponentContext} */
334
1202
  const context = {
335
1203
  props,
336
- emit: this.emitter.emit.bind(this.emitter),
337
- on: this.emitter.on.bind(this.emitter),
338
- signal: v => new Signal(v),
339
- ...this._prepareLifecycleHooks()
1204
+ emitter: this.emitter,
1205
+ /** @type {(v: unknown) => Signal<unknown>} */
1206
+ signal: v => new this.signal(v)
340
1207
  };
341
1208
 
342
1209
  /**
343
1210
  * Processes the mounting of the component.
1211
+ * This function handles:
1212
+ * 1. Merging setup data with the component context
1213
+ * 2. Setting up reactive watchers
1214
+ * 3. Rendering the component
1215
+ * 4. Managing component lifecycle
344
1216
  *
345
- * @param {object} data - Data returned from the component's setup function.
346
- * @returns {object} An object with the container, merged context data, and an unmount function.
1217
+ * @param {Object<string, unknown>} data - Data returned from the component's setup function
1218
+ * @returns {Promise<MountResult>} An object containing:
1219
+ * - container: The mounted component's container element
1220
+ * - data: The component's reactive state and context
1221
+ * - unmount: Function to clean up and unmount the component
347
1222
  */
348
- const processMount = data => {
1223
+ const processMount = async data => {
1224
+ /** @type {ComponentContext} */
349
1225
  const mergedContext = {
350
1226
  ...context,
351
1227
  ...data
352
1228
  };
353
- const watcherUnsubscribers = [];
1229
+ /** @type {Array<() => void>} */
1230
+ const watchers = [];
1231
+ /** @type {Array<MountResult>} */
354
1232
  const childInstances = [];
355
- if (!this._isMounted) {
356
- mergedContext.onBeforeMount && mergedContext.onBeforeMount();
357
- } else {
358
- mergedContext.onBeforeUpdate && mergedContext.onBeforeUpdate();
359
- }
1233
+ /** @type {Array<() => void>} */
1234
+ const listeners = [];
1235
+ /** @private {boolean} Local mounted state for this component instance */
1236
+ let isMounted = false;
360
1237
 
361
1238
  /**
362
- * Renders the component by parsing the template, patching the DOM,
363
- * processing events, injecting styles, and mounting child components.
1239
+ * Renders the component by:
1240
+ * 1. Executing lifecycle hooks
1241
+ * 2. Processing the template
1242
+ * 3. Updating the DOM
1243
+ * 4. Processing events, injecting styles, and mounting child components.
364
1244
  */
365
- const render = () => {
366
- const newHtml = TemplateEngine.parse(template(mergedContext), mergedContext);
1245
+ const render = async () => {
1246
+ // Execute before hooks
1247
+ if (!isMounted) {
1248
+ await mergedContext.onBeforeMount?.({
1249
+ container,
1250
+ context: mergedContext
1251
+ });
1252
+ } else {
1253
+ await mergedContext.onBeforeUpdate?.({
1254
+ container,
1255
+ context: mergedContext
1256
+ });
1257
+ }
1258
+ const templateResult = typeof template === "function" ? await template(mergedContext) : template;
1259
+ const newHtml = this.templateEngine.parse(templateResult, mergedContext);
367
1260
  this.renderer.patchDOM(container, newHtml);
368
- this._processEvents(container, mergedContext);
369
- this._injectStyles(container, compName, style, mergedContext);
370
- this._mountChildren(container, children, childInstances);
371
- if (!this._isMounted) {
372
- mergedContext.onMount && mergedContext.onMount();
373
- this._isMounted = true;
1261
+ this._processEvents(container, mergedContext, listeners);
1262
+ if (style) this._injectStyles(container, compId, style, mergedContext);
1263
+ if (children) await this._mountComponents(container, children, childInstances);
1264
+
1265
+ // Execute after hooks
1266
+ if (!isMounted) {
1267
+ await mergedContext.onMount?.({
1268
+ container,
1269
+ context: mergedContext
1270
+ });
1271
+ isMounted = true;
374
1272
  } else {
375
- mergedContext.onUpdate && mergedContext.onUpdate();
1273
+ await mergedContext.onUpdate?.({
1274
+ container,
1275
+ context: mergedContext
1276
+ });
376
1277
  }
377
1278
  };
378
- Object.values(data).forEach(val => {
379
- if (val instanceof Signal) watcherUnsubscribers.push(val.watch(render));
380
- });
381
- render();
382
- return {
1279
+
1280
+ /**
1281
+ * Sets up reactive watchers for all Signal instances in the component's data.
1282
+ * When a Signal's value changes, the component will re-render to reflect the updates.
1283
+ * Stores unsubscribe functions to clean up watchers when component unmounts.
1284
+ */
1285
+ for (const val of Object.values(data)) {
1286
+ if (val instanceof Signal) watchers.push(val.watch(render));
1287
+ }
1288
+ await render();
1289
+ const instance = {
383
1290
  container,
384
1291
  data: mergedContext,
385
1292
  /**
386
- * Unmounts the component, cleaning up watchers, child components, and clearing the container.
1293
+ * Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
1294
+ *
1295
+ * @returns {void}
387
1296
  */
388
- unmount: () => {
389
- watcherUnsubscribers.forEach(fn => fn());
390
- childInstances.forEach(child => child.unmount());
391
- mergedContext.onUnmount && mergedContext.onUnmount();
1297
+ unmount: async () => {
1298
+ /** @type {UnmountHookContext} */
1299
+ await mergedContext.onUnmount?.({
1300
+ container,
1301
+ context: mergedContext,
1302
+ cleanup: {
1303
+ watchers: watchers,
1304
+ listeners: listeners,
1305
+ children: childInstances
1306
+ }
1307
+ });
1308
+ for (const fn of watchers) fn();
1309
+ for (const fn of listeners) fn();
1310
+ for (const child of childInstances) await child.unmount();
392
1311
  container.innerHTML = "";
1312
+ delete container._eleva_instance;
393
1313
  }
394
1314
  };
1315
+ container._eleva_instance = instance;
1316
+ return instance;
395
1317
  };
396
1318
 
397
- // Handle asynchronous setup if needed.
398
- const setupResult = setup(context);
399
- if (setupResult && typeof setupResult.then === "function") {
400
- return setupResult.then(data => processMount(data));
401
- } else {
402
- const data = setupResult || {};
403
- return processMount(data);
404
- }
1319
+ // Handle asynchronous setup.
1320
+ const setupResult = typeof setup === "function" ? await setup(context) : {};
1321
+ return await processMount(setupResult);
405
1322
  }
406
1323
 
407
1324
  /**
408
- * Prepares default no-operation lifecycle hook functions.
1325
+ * Processes DOM elements for event binding based on attributes starting with "@".
1326
+ * This method handles the event delegation system and ensures proper cleanup of event listeners.
409
1327
  *
410
- * @returns {object} An object with keys for lifecycle hooks mapped to empty functions.
411
1328
  * @private
1329
+ * @param {HTMLElement} container - The container element in which to search for event attributes.
1330
+ * @param {ComponentContext} context - The current component context containing event handler definitions.
1331
+ * @param {Array<() => void>} listeners - Array to collect cleanup functions for each event listener.
1332
+ * @returns {void}
412
1333
  */
413
- _prepareLifecycleHooks() {
414
- return this._lifecycleHooks.reduce((acc, hook) => {
415
- acc[hook] = () => {};
416
- return acc;
417
- }, {});
1334
+ _processEvents(container, context, listeners) {
1335
+ /** @type {NodeListOf<Element>} */
1336
+ const elements = container.querySelectorAll("*");
1337
+ for (const el of elements) {
1338
+ /** @type {NamedNodeMap} */
1339
+ const attrs = el.attributes;
1340
+ for (let i = 0; i < attrs.length; i++) {
1341
+ /** @type {Attr} */
1342
+ const attr = attrs[i];
1343
+ if (!attr.name.startsWith("@")) continue;
1344
+
1345
+ /** @type {keyof HTMLElementEventMap} */
1346
+ const event = attr.name.slice(1);
1347
+ /** @type {string} */
1348
+ const handlerName = attr.value;
1349
+ /** @type {(event: Event) => void} */
1350
+ const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
1351
+ if (typeof handler === "function") {
1352
+ el.addEventListener(event, handler);
1353
+ el.removeAttribute(attr.name);
1354
+ listeners.push(() => el.removeEventListener(event, handler));
1355
+ }
1356
+ }
1357
+ }
418
1358
  }
419
1359
 
420
1360
  /**
421
- * Processes DOM elements for event binding based on attributes starting with "@".
1361
+ * Injects scoped styles into the component's container.
1362
+ * The styles are automatically prefixed to prevent style leakage to other components.
422
1363
  *
423
- * @param {HTMLElement} container - The container element in which to search for events.
424
- * @param {object} context - The current context containing event handler definitions.
425
1364
  * @private
1365
+ * @param {HTMLElement} container - The container element where styles should be injected.
1366
+ * @param {string} compId - The component ID used to identify the style element.
1367
+ * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
1368
+ * @param {ComponentContext} context - The current component context for style interpolation.
1369
+ * @returns {void}
426
1370
  */
427
- _processEvents(container, context) {
428
- container.querySelectorAll("*").forEach(el => {
429
- [...el.attributes].forEach(({
430
- name,
431
- value
432
- }) => {
433
- if (name.startsWith("@")) {
434
- const event = name.slice(1);
435
- const handler = TemplateEngine.evaluate(value, context);
436
- if (typeof handler === "function") {
437
- el.addEventListener(event, handler);
438
- el.removeAttribute(name);
439
- }
440
- }
441
- });
442
- });
1371
+ _injectStyles(container, compId, styleDef, context) {
1372
+ /** @type {string} */
1373
+ const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
1374
+
1375
+ /** @type {HTMLStyleElement|null} */
1376
+ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
1377
+ if (styleEl && styleEl.textContent === newStyle) return;
1378
+ if (!styleEl) {
1379
+ styleEl = document.createElement("style");
1380
+ styleEl.setAttribute("data-e-style", compId);
1381
+ container.appendChild(styleEl);
1382
+ }
1383
+ styleEl.textContent = newStyle;
443
1384
  }
444
1385
 
445
1386
  /**
446
- * Injects scoped styles into the component's container.
1387
+ * Extracts props from an element's attributes that start with the specified prefix.
1388
+ * This method is used to collect component properties from DOM elements.
447
1389
  *
448
- * @param {HTMLElement} container - The container element.
449
- * @param {string} compName - The component name used to identify the style element.
450
- * @param {Function} styleFn - A function that returns CSS styles as a string.
451
- * @param {object} context - The current context for style interpolation.
452
1390
  * @private
1391
+ * @param {HTMLElement} element - The DOM element to extract props from
1392
+ * @returns {Record<string, string>} An object containing the extracted props
1393
+ * @example
1394
+ * // For an element with attributes:
1395
+ * // <div :name="John" :age="25">
1396
+ * // Returns: { name: "John", age: "25" }
453
1397
  */
454
- _injectStyles(container, compName, styleFn, context) {
455
- if (styleFn) {
456
- let styleEl = container.querySelector(`style[data-eleva-style="${compName}"]`);
457
- if (!styleEl) {
458
- styleEl = document.createElement("style");
459
- styleEl.setAttribute("data-eleva-style", compName);
460
- container.appendChild(styleEl);
1398
+ _extractProps(element) {
1399
+ if (!element.attributes) return {};
1400
+ const props = {};
1401
+ const attrs = element.attributes;
1402
+ for (let i = attrs.length - 1; i >= 0; i--) {
1403
+ const attr = attrs[i];
1404
+ if (attr.name.startsWith(":")) {
1405
+ const propName = attr.name.slice(1);
1406
+ props[propName] = attr.value;
1407
+ element.removeAttribute(attr.name);
461
1408
  }
462
- styleEl.textContent = TemplateEngine.parse(styleFn(context), context);
463
1409
  }
1410
+ return props;
464
1411
  }
465
1412
 
466
1413
  /**
467
- * Mounts child components within the parent component's container.
1414
+ * Mounts all components within the parent component's container.
1415
+ * This method handles mounting of explicitly defined children components.
1416
+ *
1417
+ * The mounting process follows these steps:
1418
+ * 1. Cleans up any existing component instances
1419
+ * 2. Mounts explicitly defined children components
468
1420
  *
469
- * @param {HTMLElement} container - The parent container element.
470
- * @param {object} children - An object mapping child component selectors to their definitions.
471
- * @param {Array} childInstances - An array to store the mounted child component instances.
472
1421
  * @private
1422
+ * @param {HTMLElement} container - The container element to mount components in
1423
+ * @param {Object<string, ComponentDefinition>} children - Map of selectors to component definitions for explicit children
1424
+ * @param {Array<MountResult>} childInstances - Array to store all mounted component instances
1425
+ * @returns {Promise<void>}
1426
+ *
1427
+ * @example
1428
+ * // Explicit children mounting:
1429
+ * const children = {
1430
+ * 'UserProfile': UserProfileComponent,
1431
+ * '#settings-panel': "settings-panel"
1432
+ * };
473
1433
  */
474
- _mountChildren(container, children, childInstances) {
475
- childInstances.forEach(child => child.unmount());
476
- childInstances.length = 0;
477
- Object.keys(children || {}).forEach(childName => {
478
- container.querySelectorAll(childName).forEach(childEl => {
479
- const props = {};
480
- [...childEl.attributes].forEach(({
481
- name,
482
- value
483
- }) => {
484
- if (name.startsWith("eleva-prop-")) {
485
- props[name.slice("eleva-prop-".length)] = value;
486
- }
487
- });
488
- const instance = this.mount(childEl, childName, props);
489
- childInstances.push(instance);
490
- });
491
- });
1434
+ async _mountComponents(container, children, childInstances) {
1435
+ for (const [selector, component] of Object.entries(children)) {
1436
+ if (!selector) continue;
1437
+ for (const el of container.querySelectorAll(selector)) {
1438
+ if (!(el instanceof HTMLElement)) continue;
1439
+ /** @type {Record<string, string>} */
1440
+ const props = this._extractProps(el);
1441
+ /** @type {MountResult} */
1442
+ const instance = await this.mount(el, component, props);
1443
+ if (instance && !childInstances.includes(instance)) {
1444
+ childInstances.push(instance);
1445
+ }
1446
+ }
1447
+ }
492
1448
  }
493
1449
  }
494
1450