eleva 1.0.0-alpha → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/Eleva.js CHANGED
@@ -6,241 +6,489 @@ import { Emitter } from "../modules/Emitter.js";
6
6
  import { Renderer } from "../modules/Renderer.js";
7
7
 
8
8
  /**
9
- * 🧩 Eleva Core: Signal-based component runtime framework with lifecycle, scoped styles, and plugins.
9
+ * @typedef {Object} ComponentDefinition
10
+ * @property {function(ComponentContext): (Record<string, unknown>|Promise<Record<string, unknown>>)} [setup]
11
+ * Optional setup function that initializes the component's state and returns reactive data
12
+ * @property {(function(ComponentContext): string|Promise<string>)} template
13
+ * Required function that defines the component's HTML structure
14
+ * @property {(function(ComponentContext): string)|string} [style]
15
+ * Optional function or string that provides component-scoped CSS styles
16
+ * @property {Record<string, ComponentDefinition>} [children]
17
+ * Optional object defining nested child components
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} ComponentContext
22
+ * @property {Record<string, unknown>} props
23
+ * Component properties passed during mounting
24
+ * @property {Emitter} emitter
25
+ * Event emitter instance for component event handling
26
+ * @property {function<T>(value: T): Signal<T>} signal
27
+ * Factory function to create reactive Signal instances
28
+ * @property {function(LifecycleHookContext): Promise<void>} [onBeforeMount]
29
+ * Hook called before component mounting
30
+ * @property {function(LifecycleHookContext): Promise<void>} [onMount]
31
+ * Hook called after component mounting
32
+ * @property {function(LifecycleHookContext): Promise<void>} [onBeforeUpdate]
33
+ * Hook called before component update
34
+ * @property {function(LifecycleHookContext): Promise<void>} [onUpdate]
35
+ * Hook called after component update
36
+ * @property {function(UnmountHookContext): Promise<void>} [onUnmount]
37
+ * Hook called during component unmounting
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} LifecycleHookContext
42
+ * @property {HTMLElement} container
43
+ * The DOM element where the component is mounted
44
+ * @property {ComponentContext} context
45
+ * The component's reactive state and context data
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} UnmountHookContext
50
+ * @property {HTMLElement} container
51
+ * The DOM element where the component is mounted
52
+ * @property {ComponentContext} context
53
+ * The component's reactive state and context data
54
+ * @property {{
55
+ * watchers: Array<() => void>, // Signal watcher cleanup functions
56
+ * listeners: Array<() => void>, // Event listener cleanup functions
57
+ * children: Array<MountResult> // Child component instances
58
+ * }} cleanup
59
+ * Object containing cleanup functions and instances
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} MountResult
64
+ * @property {HTMLElement} container
65
+ * The DOM element where the component is mounted
66
+ * @property {ComponentContext} data
67
+ * The component's reactive state and context data
68
+ * @property {function(): Promise<void>} unmount
69
+ * Function to clean up and unmount the component
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} ElevaPlugin
74
+ * @property {function(Eleva, Record<string, unknown>): void} install
75
+ * Function that installs the plugin into the Eleva instance
76
+ * @property {string} name
77
+ * Unique identifier name for the plugin
78
+ */
79
+
80
+ /**
81
+ * @class 🧩 Eleva
82
+ * @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
83
+ * scoped styles, and plugin support. Eleva manages component registration, plugin integration,
84
+ * event handling, and DOM rendering with a focus on performance and developer experience.
10
85
  *
11
- * The Eleva class is the core of the framework. It manages component registration,
12
- * plugin integration, lifecycle hooks, event handling, and DOM rendering.
86
+ * @example
87
+ * // Basic component creation and mounting
88
+ * const app = new Eleva("myApp");
89
+ * app.component("myComponent", {
90
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
91
+ * template: (ctx) => `<div>Hello ${ctx.props.name}</div>`
92
+ * });
93
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
94
+ *
95
+ * @example
96
+ * // Using lifecycle hooks
97
+ * app.component("lifecycleDemo", {
98
+ * setup: () => {
99
+ * return {
100
+ * onMount: ({ container, context }) => {
101
+ * console.log('Component mounted!');
102
+ * }
103
+ * };
104
+ * },
105
+ * template: `<div>Lifecycle Demo</div>`
106
+ * });
13
107
  */
14
108
  export class Eleva {
15
109
  /**
16
- * Creates a new Eleva instance.
110
+ * Creates a new Eleva instance with the specified name and configuration.
111
+ *
112
+ * @public
113
+ * @param {string} name - The unique identifier name for this Eleva instance.
114
+ * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
115
+ * May include framework-wide settings and default behaviors.
116
+ * @throws {Error} If the name is not provided or is not a string.
117
+ * @returns {Eleva} A new Eleva instance.
118
+ *
119
+ * @example
120
+ * const app = new Eleva("myApp");
121
+ * app.component("myComponent", {
122
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
123
+ * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
124
+ * });
125
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
17
126
  *
18
- * @param {string} name - The name of the Eleva instance.
19
- * @param {object} [config={}] - Optional configuration for the instance.
20
127
  */
21
128
  constructor(name, config = {}) {
129
+ /** @public {string} The unique identifier name for this Eleva instance */
22
130
  this.name = name;
131
+ /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */
23
132
  this.config = config;
24
- this._components = {};
25
- this._plugins = [];
26
- this._lifecycleHooks = [
27
- "onBeforeMount",
28
- "onMount",
29
- "onBeforeUpdate",
30
- "onUpdate",
31
- "onUnmount",
32
- ];
33
- this._isMounted = false;
133
+ /** @public {Emitter} Instance of the event emitter for handling component events */
34
134
  this.emitter = new Emitter();
135
+ /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
136
+ this.signal = Signal;
137
+ /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
35
138
  this.renderer = new Renderer();
139
+
140
+ /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */
141
+ this._components = new Map();
142
+ /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
143
+ this._plugins = new Map();
144
+ /** @private {boolean} Flag indicating if the root component is currently mounted */
145
+ this._isMounted = false;
146
+ /** @private {number} Counter for generating unique component IDs */
147
+ this._componentCounter = 0;
36
148
  }
37
149
 
38
150
  /**
39
151
  * Integrates a plugin with the Eleva framework.
152
+ * The plugin's install function will be called with the Eleva instance and provided options.
153
+ * After installation, the plugin will be available for use by components.
40
154
  *
41
- * @param {object} [plugin] - The plugin object which should have an install function.
42
- * @param {object} [options={}] - Optional options to pass to the plugin.
43
- * @returns {Eleva} The Eleva instance (for chaining).
155
+ * @public
156
+ * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
157
+ * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
158
+ * @returns {Eleva} The Eleva instance (for method chaining).
159
+ * @example
160
+ * app.use(myPlugin, { option1: "value1" });
44
161
  */
45
162
  use(plugin, options = {}) {
46
- if (typeof plugin.install === "function") {
47
- plugin.install(this, options);
48
- }
49
- this._plugins.push(plugin);
163
+ plugin.install(this, options);
164
+ this._plugins.set(plugin.name, plugin);
165
+
50
166
  return this;
51
167
  }
52
168
 
53
169
  /**
54
- * Registers a component with the Eleva instance.
170
+ * Registers a new component with the Eleva instance.
171
+ * The component will be available for mounting using its registered name.
55
172
  *
56
- * @param {string} name - The name of the component.
57
- * @param {object} definition - The component definition including setup, template, style, and children.
58
- * @returns {Eleva} The Eleva instance (for chaining).
173
+ * @public
174
+ * @param {string} name - The unique name of the component to register.
175
+ * @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
176
+ * @returns {Eleva} The Eleva instance (for method chaining).
177
+ * @throws {Error} If the component name is already registered.
178
+ * @example
179
+ * app.component("myButton", {
180
+ * template: (ctx) => `<button>${ctx.props.text}</button>`,
181
+ * style: `button { color: blue; }`
182
+ * });
59
183
  */
60
184
  component(name, definition) {
61
- this._components[name] = definition;
185
+ /** @type {Map<string, ComponentDefinition>} */
186
+ this._components.set(name, definition);
62
187
  return this;
63
188
  }
64
189
 
65
190
  /**
66
191
  * Mounts a registered component to a DOM element.
192
+ * This will initialize the component, set up its reactive state, and render it to the DOM.
67
193
  *
68
- * @param {string|HTMLElement} selectorOrElement - A CSS selector string or DOM element where the component will be mounted.
69
- * @param {string} compName - The name of the component to mount.
70
- * @param {object} [props={}] - Optional properties to pass to the component.
71
- * @returns {object|Promise<object>} An object representing the mounted component instance, or a Promise that resolves to it for asynchronous setups.
72
- * @throws Will throw an error if the container or component is not found.
194
+ * @public
195
+ * @param {HTMLElement} container - The DOM element where the component will be mounted.
196
+ * @param {string|ComponentDefinition} compName - The name of the registered component or a direct component definition.
197
+ * @param {Object<string, unknown>} [props={}] - Optional properties to pass to the component.
198
+ * @returns {Promise<MountResult>}
199
+ * A Promise that resolves to an object containing:
200
+ * - container: The mounted component's container element
201
+ * - data: The component's reactive state and context
202
+ * - unmount: Function to clean up and unmount the component
203
+ * @throws {Error} If the container is not found, or component is not registered.
204
+ * @example
205
+ * const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
206
+ * // Later...
207
+ * instance.unmount();
73
208
  */
74
- mount(selectorOrElement, compName, props = {}) {
75
- const container =
76
- typeof selectorOrElement === "string"
77
- ? document.querySelector(selectorOrElement)
78
- : selectorOrElement;
79
- if (!container)
80
- throw new Error(`Container not found: ${selectorOrElement}`);
81
-
82
- const definition = this._components[compName];
209
+ async mount(container, compName, props = {}) {
210
+ if (!container) throw new Error(`Container not found: ${container}`);
211
+
212
+ if (container._eleva_instance) return container._eleva_instance;
213
+
214
+ /** @type {ComponentDefinition} */
215
+ const definition =
216
+ typeof compName === "string" ? this._components.get(compName) : compName;
83
217
  if (!definition) throw new Error(`Component "${compName}" not registered.`);
84
218
 
219
+ /** @type {string} */
220
+ const compId = `c${++this._componentCounter}`;
221
+
222
+ /**
223
+ * Destructure the component definition to access core functionality.
224
+ * - setup: Optional function for component initialization and state management
225
+ * - template: Required function or string that returns the component's HTML structure
226
+ * - style: Optional function or string for component-scoped CSS styles
227
+ * - children: Optional object defining nested child components
228
+ */
85
229
  const { setup, template, style, children } = definition;
230
+
231
+ /** @type {ComponentContext} */
86
232
  const context = {
87
233
  props,
88
- emit: this.emitter.emit.bind(this.emitter),
89
- on: this.emitter.on.bind(this.emitter),
90
- signal: (v) => new Signal(v),
91
- ...this._prepareLifecycleHooks(),
234
+ emitter: this.emitter,
235
+ /** @type {(v: unknown) => Signal<unknown>} */
236
+ signal: (v) => new this.signal(v),
92
237
  };
93
238
 
94
239
  /**
95
240
  * Processes the mounting of the component.
241
+ * This function handles:
242
+ * 1. Merging setup data with the component context
243
+ * 2. Setting up reactive watchers
244
+ * 3. Rendering the component
245
+ * 4. Managing component lifecycle
96
246
  *
97
- * @param {object} data - Data returned from the component's setup function.
98
- * @returns {object} An object with the container, merged context data, and an unmount function.
247
+ * @param {Object<string, unknown>} data - Data returned from the component's setup function
248
+ * @returns {Promise<MountResult>} An object containing:
249
+ * - container: The mounted component's container element
250
+ * - data: The component's reactive state and context
251
+ * - unmount: Function to clean up and unmount the component
99
252
  */
100
- const processMount = (data) => {
253
+ const processMount = async (data) => {
254
+ /** @type {ComponentContext} */
101
255
  const mergedContext = { ...context, ...data };
102
- const watcherUnsubscribers = [];
256
+ /** @type {Array<() => void>} */
257
+ const watchers = [];
258
+ /** @type {Array<MountResult>} */
103
259
  const childInstances = [];
260
+ /** @type {Array<() => void>} */
261
+ const listeners = [];
104
262
 
263
+ // Execute before hooks
105
264
  if (!this._isMounted) {
106
- mergedContext.onBeforeMount && mergedContext.onBeforeMount();
265
+ /** @type {LifecycleHookContext} */
266
+ await mergedContext.onBeforeMount?.({
267
+ container,
268
+ context: mergedContext,
269
+ });
107
270
  } else {
108
- mergedContext.onBeforeUpdate && mergedContext.onBeforeUpdate();
271
+ /** @type {LifecycleHookContext} */
272
+ await mergedContext.onBeforeUpdate?.({
273
+ container,
274
+ context: mergedContext,
275
+ });
109
276
  }
110
277
 
111
278
  /**
112
- * Renders the component by parsing the template, patching the DOM,
113
- * processing events, injecting styles, and mounting child components.
279
+ * Renders the component by:
280
+ * 1. Processing the template
281
+ * 2. Updating the DOM
282
+ * 3. Processing events, injecting styles, and mounting child components.
114
283
  */
115
- const render = () => {
116
- const newHtml = TemplateEngine.parse(
117
- template(mergedContext),
118
- mergedContext
119
- );
284
+ const render = async () => {
285
+ const templateResult =
286
+ typeof template === "function"
287
+ ? await template(mergedContext)
288
+ : template;
289
+ const newHtml = TemplateEngine.parse(templateResult, mergedContext);
120
290
  this.renderer.patchDOM(container, newHtml);
121
- this._processEvents(container, mergedContext);
122
- this._injectStyles(container, compName, style, mergedContext);
123
- this._mountChildren(container, children, childInstances);
291
+ this._processEvents(container, mergedContext, listeners);
292
+ if (style) this._injectStyles(container, compId, style, mergedContext);
293
+ if (children)
294
+ await this._mountComponents(container, children, childInstances);
295
+
124
296
  if (!this._isMounted) {
125
- mergedContext.onMount && mergedContext.onMount();
297
+ /** @type {LifecycleHookContext} */
298
+ await mergedContext.onMount?.({
299
+ container,
300
+ context: mergedContext,
301
+ });
126
302
  this._isMounted = true;
127
303
  } else {
128
- mergedContext.onUpdate && mergedContext.onUpdate();
304
+ /** @type {LifecycleHookContext} */
305
+ await mergedContext.onUpdate?.({
306
+ container,
307
+ context: mergedContext,
308
+ });
129
309
  }
130
310
  };
131
311
 
132
- Object.values(data).forEach((val) => {
133
- if (val instanceof Signal) watcherUnsubscribers.push(val.watch(render));
134
- });
312
+ /**
313
+ * Sets up reactive watchers for all Signal instances in the component's data.
314
+ * When a Signal's value changes, the component will re-render to reflect the updates.
315
+ * Stores unsubscribe functions to clean up watchers when component unmounts.
316
+ */
317
+ for (const val of Object.values(data)) {
318
+ if (val instanceof Signal) watchers.push(val.watch(render));
319
+ }
135
320
 
136
- render();
321
+ await render();
137
322
 
138
- return {
323
+ const instance = {
139
324
  container,
140
325
  data: mergedContext,
141
326
  /**
142
- * Unmounts the component, cleaning up watchers, child components, and clearing the container.
327
+ * Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
328
+ *
329
+ * @returns {void}
143
330
  */
144
- unmount: () => {
145
- watcherUnsubscribers.forEach((fn) => fn());
146
- childInstances.forEach((child) => child.unmount());
147
- mergedContext.onUnmount && mergedContext.onUnmount();
331
+ unmount: async () => {
332
+ /** @type {UnmountHookContext} */
333
+ await mergedContext.onUnmount?.({
334
+ container,
335
+ context: mergedContext,
336
+ cleanup: {
337
+ watchers: watchers,
338
+ listeners: listeners,
339
+ children: childInstances,
340
+ },
341
+ });
342
+ for (const fn of watchers) fn();
343
+ for (const fn of listeners) fn();
344
+ for (const child of childInstances) await child.unmount();
148
345
  container.innerHTML = "";
346
+ delete container._eleva_instance;
149
347
  },
150
348
  };
349
+
350
+ container._eleva_instance = instance;
351
+ return instance;
151
352
  };
152
353
 
153
- // Handle asynchronous setup if needed.
154
- const setupResult = setup(context);
155
- if (setupResult && typeof setupResult.then === "function") {
156
- return setupResult.then((data) => processMount(data));
157
- } else {
158
- const data = setupResult || {};
159
- return processMount(data);
160
- }
354
+ // Handle asynchronous setup.
355
+ const setupResult = typeof setup === "function" ? await setup(context) : {};
356
+ return await processMount(setupResult);
161
357
  }
162
358
 
163
359
  /**
164
- * Prepares default no-operation lifecycle hook functions.
360
+ * Processes DOM elements for event binding based on attributes starting with "@".
361
+ * This method handles the event delegation system and ensures proper cleanup of event listeners.
165
362
  *
166
- * @returns {object} An object with keys for lifecycle hooks mapped to empty functions.
167
363
  * @private
364
+ * @param {HTMLElement} container - The container element in which to search for event attributes.
365
+ * @param {ComponentContext} context - The current component context containing event handler definitions.
366
+ * @param {Array<() => void>} listeners - Array to collect cleanup functions for each event listener.
367
+ * @returns {void}
168
368
  */
169
- _prepareLifecycleHooks() {
170
- return this._lifecycleHooks.reduce((acc, hook) => {
171
- acc[hook] = () => {};
172
- return acc;
173
- }, {});
369
+ _processEvents(container, context, listeners) {
370
+ /** @type {NodeListOf<Element>} */
371
+ const elements = container.querySelectorAll("*");
372
+ for (const el of elements) {
373
+ /** @type {NamedNodeMap} */
374
+ const attrs = el.attributes;
375
+ for (let i = 0; i < attrs.length; i++) {
376
+ /** @type {Attr} */
377
+ const attr = attrs[i];
378
+
379
+ if (!attr.name.startsWith("@")) continue;
380
+
381
+ /** @type {keyof HTMLElementEventMap} */
382
+ const event = attr.name.slice(1);
383
+ /** @type {string} */
384
+ const handlerName = attr.value;
385
+ /** @type {(event: Event) => void} */
386
+ const handler =
387
+ context[handlerName] || TemplateEngine.evaluate(handlerName, context);
388
+ if (typeof handler === "function") {
389
+ el.addEventListener(event, handler);
390
+ el.removeAttribute(attr.name);
391
+ listeners.push(() => el.removeEventListener(event, handler));
392
+ }
393
+ }
394
+ }
174
395
  }
175
396
 
176
397
  /**
177
- * Processes DOM elements for event binding based on attributes starting with "@".
398
+ * Injects scoped styles into the component's container.
399
+ * The styles are automatically prefixed to prevent style leakage to other components.
178
400
  *
179
- * @param {HTMLElement} container - The container element in which to search for events.
180
- * @param {object} context - The current context containing event handler definitions.
181
401
  * @private
402
+ * @param {HTMLElement} container - The container element where styles should be injected.
403
+ * @param {string} compId - The component ID used to identify the style element.
404
+ * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
405
+ * @param {ComponentContext} context - The current component context for style interpolation.
406
+ * @returns {void}
182
407
  */
183
- _processEvents(container, context) {
184
- container.querySelectorAll("*").forEach((el) => {
185
- [...el.attributes].forEach(({ name, value }) => {
186
- if (name.startsWith("@")) {
187
- const event = name.slice(1);
188
- const handler = TemplateEngine.evaluate(value, context);
189
- if (typeof handler === "function") {
190
- el.addEventListener(event, handler);
191
- el.removeAttribute(name);
192
- }
193
- }
194
- });
195
- });
408
+ _injectStyles(container, compId, styleDef, context) {
409
+ /** @type {string} */
410
+ const newStyle =
411
+ typeof styleDef === "function"
412
+ ? TemplateEngine.parse(styleDef(context), context)
413
+ : styleDef;
414
+
415
+ /** @type {HTMLStyleElement|null} */
416
+ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
417
+
418
+ if (styleEl && styleEl.textContent === newStyle) return;
419
+ if (!styleEl) {
420
+ styleEl = document.createElement("style");
421
+ styleEl.setAttribute("data-e-style", compId);
422
+ container.appendChild(styleEl);
423
+ }
424
+
425
+ styleEl.textContent = newStyle;
196
426
  }
197
427
 
198
428
  /**
199
- * Injects scoped styles into the component's container.
429
+ * Extracts props from an element's attributes that start with the specified prefix.
430
+ * This method is used to collect component properties from DOM elements.
200
431
  *
201
- * @param {HTMLElement} container - The container element.
202
- * @param {string} compName - The component name used to identify the style element.
203
- * @param {Function} styleFn - A function that returns CSS styles as a string.
204
- * @param {object} context - The current context for style interpolation.
205
432
  * @private
433
+ * @param {HTMLElement} element - The DOM element to extract props from
434
+ * @param {string} prefix - The prefix to look for in attributes
435
+ * @returns {Record<string, string>} An object containing the extracted props
436
+ * @example
437
+ * // For an element with attributes:
438
+ * // <div :name="John" :age="25">
439
+ * // Returns: { name: "John", age: "25" }
206
440
  */
207
- _injectStyles(container, compName, styleFn, context) {
208
- if (styleFn) {
209
- let styleEl = container.querySelector(
210
- `style[data-eleva-style="${compName}"]`
211
- );
212
- if (!styleEl) {
213
- styleEl = document.createElement("style");
214
- styleEl.setAttribute("data-eleva-style", compName);
215
- container.appendChild(styleEl);
441
+ _extractProps(element, prefix) {
442
+ if (!element.attributes) return {};
443
+
444
+ const props = {};
445
+ const attrs = element.attributes;
446
+
447
+ for (let i = attrs.length - 1; i >= 0; i--) {
448
+ const attr = attrs[i];
449
+ if (attr.name.startsWith(prefix)) {
450
+ const propName = attr.name.slice(prefix.length);
451
+ props[propName] = attr.value;
452
+ element.removeAttribute(attr.name);
216
453
  }
217
- styleEl.textContent = TemplateEngine.parse(styleFn(context), context);
218
454
  }
455
+ return props;
219
456
  }
220
457
 
221
458
  /**
222
- * Mounts child components within the parent component's container.
459
+ * Mounts all components within the parent component's container.
460
+ * This method handles mounting of explicitly defined children components.
461
+ *
462
+ * The mounting process follows these steps:
463
+ * 1. Cleans up any existing component instances
464
+ * 2. Mounts explicitly defined children components
223
465
  *
224
- * @param {HTMLElement} container - The parent container element.
225
- * @param {object} children - An object mapping child component selectors to their definitions.
226
- * @param {Array} childInstances - An array to store the mounted child component instances.
227
466
  * @private
467
+ * @param {HTMLElement} container - The container element to mount components in
468
+ * @param {Object<string, ComponentDefinition>} children - Map of selectors to component definitions for explicit children
469
+ * @param {Array<MountResult>} childInstances - Array to store all mounted component instances
470
+ * @returns {Promise<void>}
471
+ *
472
+ * @example
473
+ * // Explicit children mounting:
474
+ * const children = {
475
+ * 'UserProfile': UserProfileComponent,
476
+ * '#settings-panel': "settings-panel"
477
+ * };
228
478
  */
229
- _mountChildren(container, children, childInstances) {
230
- childInstances.forEach((child) => child.unmount());
231
- childInstances.length = 0;
232
-
233
- Object.keys(children || {}).forEach((childName) => {
234
- container.querySelectorAll(childName).forEach((childEl) => {
235
- const props = {};
236
- [...childEl.attributes].forEach(({ name, value }) => {
237
- if (name.startsWith("eleva-prop-")) {
238
- props[name.slice("eleva-prop-".length)] = value;
239
- }
240
- });
241
- const instance = this.mount(childEl, childName, props);
242
- childInstances.push(instance);
243
- });
244
- });
479
+ async _mountComponents(container, children, childInstances) {
480
+ for (const [selector, component] of Object.entries(children)) {
481
+ if (!selector) continue;
482
+ for (const el of container.querySelectorAll(selector)) {
483
+ if (!(el instanceof HTMLElement)) continue;
484
+ /** @type {Record<string, string>} */
485
+ const props = this._extractProps(el, ":");
486
+ /** @type {MountResult} */
487
+ const instance = await this.mount(el, component, props);
488
+ if (instance && !childInstances.includes(instance)) {
489
+ childInstances.push(instance);
490
+ }
491
+ }
492
+ }
245
493
  }
246
494
  }