eleva 1.2.14-beta → 1.2.16-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/eleva.d.ts CHANGED
@@ -19,13 +19,13 @@ declare class Emitter {
19
19
  * @public
20
20
  * @param {string} event - The name of the event to listen for.
21
21
  * @param {function(any): void} handler - The callback function to invoke when the event occurs.
22
- * @returns {function(): boolean} A function to unsubscribe the event handler.
22
+ * @returns {function(): void} A function to unsubscribe the event handler.
23
23
  * @example
24
24
  * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
25
25
  * // Later...
26
26
  * unsubscribe(); // Stops listening for the event
27
27
  */
28
- public on(event: string, handler: (arg0: any) => void): () => boolean;
28
+ public on(event: string, handler: (arg0: any) => void): () => void;
29
29
  /**
30
30
  * Removes an event handler for the specified event name.
31
31
  * If no handler is provided, all handlers for the event are removed.
@@ -58,8 +58,9 @@ declare class Emitter {
58
58
  * const count = new Signal(0);
59
59
  * count.watch((value) => console.log(`Count changed to: ${value}`));
60
60
  * count.value = 1; // Logs: "Count changed to: 1"
61
+ * @template T
61
62
  */
62
- declare class Signal {
63
+ declare class Signal<T> {
63
64
  /**
64
65
  * Creates a new Signal instance with the specified initial value.
65
66
  *
@@ -126,16 +127,18 @@ declare class Signal {
126
127
  * renderer.patchDOM(container, newHtml);
127
128
  */
128
129
  declare class Renderer {
130
+ /** @private {HTMLElement} Reusable temporary container for parsing new HTML */
131
+ private _tempContainer;
129
132
  /**
130
133
  * Patches the DOM of a container element with new HTML content.
131
- * This method efficiently updates the DOM by comparing the new content with the existing
132
- * content and applying only the necessary changes.
134
+ * Efficiently updates the DOM by parsing new HTML into a reusable container
135
+ * and applying only the necessary changes.
133
136
  *
134
137
  * @public
135
138
  * @param {HTMLElement} container - The container element to patch.
136
139
  * @param {string} newHtml - The new HTML content to apply.
137
140
  * @returns {void}
138
- * @throws {Error} If container is not an HTMLElement or newHtml is not a string.
141
+ * @throws {Error} If container is not an HTMLElement, newHtml is not a string, or patching fails.
139
142
  */
140
143
  public patchDOM(container: HTMLElement, newHtml: string): void;
141
144
  /**
@@ -147,7 +150,6 @@ declare class Renderer {
147
150
  * @param {HTMLElement} oldParent - The original DOM element.
148
151
  * @param {HTMLElement} newParent - The new DOM element.
149
152
  * @returns {void}
150
- * @throws {Error} If either parent is not an HTMLElement.
151
153
  */
152
154
  private _diff;
153
155
  /**
@@ -158,7 +160,6 @@ declare class Renderer {
158
160
  * @param {HTMLElement} oldEl - The element to update.
159
161
  * @param {HTMLElement} newEl - The element providing the updated attributes.
160
162
  * @returns {void}
161
- * @throws {Error} If either element is not an HTMLElement.
162
163
  */
163
164
  private _updateAttributes;
164
165
  }
@@ -167,7 +168,7 @@ declare class Renderer {
167
168
  * @typedef {Object} ComponentDefinition
168
169
  * @property {function(Object<string, any>): (Object<string, any>|Promise<Object<string, any>>)} [setup]
169
170
  * Optional setup function that initializes the component's state and returns reactive data
170
- * @property {function(Object<string, any>): string} template
171
+ * @property {function(Object<string, any>): string|Promise<string>} template
171
172
  * Required function that defines the component's HTML structure
172
173
  * @property {function(Object<string, any>): string} [style]
173
174
  * Optional function that provides component-scoped CSS styles
@@ -335,30 +336,6 @@ declare class Eleva {
335
336
  * // Returns: { name: "John", age: "25" }
336
337
  */
337
338
  private _extractProps;
338
- /**
339
- * Mounts a single component instance to a container element.
340
- * This method handles the actual mounting of a component with its props.
341
- *
342
- * @private
343
- * @param {HTMLElement} container - The container element to mount the component to
344
- * @param {string|ComponentDefinition} component - The component to mount, either as a name or definition
345
- * @param {Object<string, any>} props - The props to pass to the component
346
- * @returns {Promise<MountResult>} A promise that resolves to the mounted component instance
347
- * @throws {Error} If the container is not a valid HTMLElement
348
- */
349
- private _mountComponentInstance;
350
- /**
351
- * Mounts components found by a selector in the container.
352
- * This method handles mounting multiple instances of the same component type.
353
- *
354
- * @private
355
- * @param {HTMLElement} container - The container to search for components
356
- * @param {string} selector - The CSS selector to find components
357
- * @param {string|ComponentDefinition} component - The component to mount
358
- * @param {Array<MountResult>} instances - Array to store the mounted component instances
359
- * @returns {Promise<void>}
360
- */
361
- private _mountComponentsBySelector;
362
339
  /**
363
340
  * Mounts all components within the parent component's container.
364
341
  * This method handles mounting of explicitly defined children components.
@@ -398,7 +375,7 @@ type ComponentDefinition = {
398
375
  */
399
376
  template: (arg0: {
400
377
  [x: string]: any;
401
- }) => string;
378
+ }) => string | Promise<string>;
402
379
  /**
403
380
  * Optional function that provides component-scoped CSS styles
404
381
  */
package/dist/eleva.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Eleva v1.2.14-beta | MIT License | https://elevajs.com */
1
+ /*! Eleva v1.2.16-beta | MIT License | https://elevajs.com */
2
2
  /**
3
3
  * @class 🔒 TemplateEngine
4
4
  * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
@@ -68,6 +68,7 @@ class TemplateEngine {
68
68
  * const count = new Signal(0);
69
69
  * count.watch((value) => console.log(`Count changed to: ${value}`));
70
70
  * count.value = 1; // Logs: "Count changed to: 1"
71
+ * @template T
71
72
  */
72
73
  class Signal {
73
74
  /**
@@ -173,7 +174,7 @@ class Emitter {
173
174
  * @public
174
175
  * @param {string} event - The name of the event to listen for.
175
176
  * @param {function(any): void} handler - The callback function to invoke when the event occurs.
176
- * @returns {function(): boolean} A function to unsubscribe the event handler.
177
+ * @returns {function(): void} A function to unsubscribe the event handler.
177
178
  * @example
178
179
  * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
179
180
  * // Later...
@@ -234,16 +235,25 @@ class Emitter {
234
235
  * renderer.patchDOM(container, newHtml);
235
236
  */
236
237
  class Renderer {
238
+ /**
239
+ * Creates a new Renderer instance with a reusable temporary container for parsing HTML.
240
+ * @public
241
+ */
242
+ constructor() {
243
+ /** @private {HTMLElement} Reusable temporary container for parsing new HTML */
244
+ this._tempContainer = document.createElement("div");
245
+ }
246
+
237
247
  /**
238
248
  * Patches the DOM of a container element with new HTML content.
239
- * This method efficiently updates the DOM by comparing the new content with the existing
240
- * content and applying only the necessary changes.
249
+ * Efficiently updates the DOM by parsing new HTML into a reusable container
250
+ * and applying only the necessary changes.
241
251
  *
242
252
  * @public
243
253
  * @param {HTMLElement} container - The container element to patch.
244
254
  * @param {string} newHtml - The new HTML content to apply.
245
255
  * @returns {void}
246
- * @throws {Error} If container is not an HTMLElement or newHtml is not a string.
256
+ * @throws {Error} If container is not an HTMLElement, newHtml is not a string, or patching fails.
247
257
  */
248
258
  patchDOM(container, newHtml) {
249
259
  if (!(container instanceof HTMLElement)) {
@@ -252,10 +262,13 @@ class Renderer {
252
262
  if (typeof newHtml !== "string") {
253
263
  throw new Error("newHtml must be a string");
254
264
  }
255
- const temp = document.createElement("div");
256
- temp.innerHTML = newHtml;
257
- this._diff(container, temp);
258
- temp.innerHTML = "";
265
+ try {
266
+ // Directly set new HTML, replacing any existing content
267
+ this._tempContainer.innerHTML = newHtml;
268
+ this._diff(container, this._tempContainer);
269
+ } catch {
270
+ throw new Error("Failed to patch DOM");
271
+ }
259
272
  }
260
273
 
261
274
  /**
@@ -267,12 +280,8 @@ class Renderer {
267
280
  * @param {HTMLElement} oldParent - The original DOM element.
268
281
  * @param {HTMLElement} newParent - The new DOM element.
269
282
  * @returns {void}
270
- * @throws {Error} If either parent is not an HTMLElement.
271
283
  */
272
284
  _diff(oldParent, newParent) {
273
- if (!(oldParent instanceof HTMLElement) || !(newParent instanceof HTMLElement)) {
274
- throw new Error("Both parents must be HTMLElements");
275
- }
276
285
  if (oldParent.isEqualNode(newParent)) return;
277
286
  const oldChildren = oldParent.childNodes;
278
287
  const newChildren = newParent.childNodes;
@@ -280,7 +289,9 @@ class Renderer {
280
289
  for (let i = 0; i < maxLength; i++) {
281
290
  const oldNode = oldChildren[i];
282
291
  const newNode = newChildren[i];
283
- if (!oldNode && !newNode) continue;
292
+ if (oldNode?._eleva_instance) {
293
+ continue;
294
+ }
284
295
  if (!oldNode && newNode) {
285
296
  oldParent.appendChild(newNode.cloneNode(true));
286
297
  continue;
@@ -317,12 +328,8 @@ class Renderer {
317
328
  * @param {HTMLElement} oldEl - The element to update.
318
329
  * @param {HTMLElement} newEl - The element providing the updated attributes.
319
330
  * @returns {void}
320
- * @throws {Error} If either element is not an HTMLElement.
321
331
  */
322
332
  _updateAttributes(oldEl, newEl) {
323
- if (!(oldEl instanceof HTMLElement) || !(newEl instanceof HTMLElement)) {
324
- throw new Error("Both elements must be HTMLElements");
325
- }
326
333
  const oldAttrs = oldEl.attributes;
327
334
  const newAttrs = newEl.attributes;
328
335
 
@@ -369,7 +376,7 @@ class Renderer {
369
376
  * @typedef {Object} ComponentDefinition
370
377
  * @property {function(Object<string, any>): (Object<string, any>|Promise<Object<string, any>>)} [setup]
371
378
  * Optional setup function that initializes the component's state and returns reactive data
372
- * @property {function(Object<string, any>): string} template
379
+ * @property {function(Object<string, any>): string|Promise<string>} template
373
380
  * Required function that defines the component's HTML structure
374
381
  * @property {function(Object<string, any>): string} [style]
375
382
  * Optional function that provides component-scoped CSS styles
@@ -500,6 +507,9 @@ class Eleva {
500
507
  */
501
508
  async mount(container, compName, props = {}) {
502
509
  if (!container) throw new Error(`Container not found: ${container}`);
510
+ if (container._eleva_instance) {
511
+ return container._eleva_instance;
512
+ }
503
513
 
504
514
  /** @type {ComponentDefinition} */
505
515
  const definition = typeof compName === "string" ? this._components.get(compName) : compName;
@@ -532,7 +542,7 @@ class Eleva {
532
542
  const context = {
533
543
  props,
534
544
  emitter: this.emitter,
535
- /** @type {(v: any) => Signal} */
545
+ /** @type {(v: any) => Signal<any>} */
536
546
  signal: v => new this.signal(v),
537
547
  ...this._prepareLifecycleHooks()
538
548
  };
@@ -546,12 +556,13 @@ class Eleva {
546
556
  * 4. Managing component lifecycle
547
557
  *
548
558
  * @param {Object<string, any>} data - Data returned from the component's setup function
549
- * @returns {MountResult} An object containing:
559
+ * @returns {Promise<MountResult>} An object containing:
550
560
  * - container: The mounted component's container element
551
561
  * - data: The component's reactive state and context
552
562
  * - unmount: Function to clean up and unmount the component
553
563
  */
554
564
  const processMount = async data => {
565
+ /** @type {Object<string, any>} */
555
566
  const mergedContext = {
556
567
  ...context,
557
568
  ...data
@@ -571,15 +582,18 @@ class Eleva {
571
582
  }
572
583
 
573
584
  /**
574
- * Renders the component by parsing the template, patching the DOM,
575
- * processing events, injecting styles, and mounting child components.
585
+ * Renders the component by:
586
+ * 1. Processing the template
587
+ * 2. Updating the DOM
588
+ * 3. Processing events, injecting styles, and mounting child components.
576
589
  */
577
590
  const render = async () => {
578
- const newHtml = TemplateEngine.parse(template(mergedContext), mergedContext);
591
+ const templateResult = await template(mergedContext);
592
+ const newHtml = TemplateEngine.parse(templateResult, mergedContext);
579
593
  this.renderer.patchDOM(container, newHtml);
580
594
  this._processEvents(container, mergedContext, cleanupListeners);
581
- this._injectStyles(container, compName, style, mergedContext);
582
- await this._mountComponents(container, children, childInstances);
595
+ if (style) this._injectStyles(container, compName, style, mergedContext);
596
+ if (children) await this._mountComponents(container, children, childInstances);
583
597
  if (!this._isMounted) {
584
598
  mergedContext.onMount && mergedContext.onMount();
585
599
  this._isMounted = true;
@@ -597,7 +611,7 @@ class Eleva {
597
611
  if (val instanceof Signal) watcherUnsubscribers.push(val.watch(render));
598
612
  }
599
613
  await render();
600
- return {
614
+ const instance = {
601
615
  container,
602
616
  data: mergedContext,
603
617
  /**
@@ -611,8 +625,11 @@ class Eleva {
611
625
  for (const child of childInstances) child.unmount();
612
626
  mergedContext.onUnmount && mergedContext.onUnmount();
613
627
  container.innerHTML = "";
628
+ delete container._eleva_instance;
614
629
  }
615
630
  };
631
+ container._eleva_instance = instance;
632
+ return instance;
616
633
  };
617
634
 
618
635
  // Handle asynchronous setup.
@@ -653,14 +670,14 @@ class Eleva {
653
670
  const attrs = el.attributes;
654
671
  for (let i = 0; i < attrs.length; i++) {
655
672
  const attr = attrs[i];
656
- if (attr.name.startsWith("@")) {
657
- const event = attr.name.slice(1);
658
- const handler = TemplateEngine.evaluate(attr.value, context);
659
- if (typeof handler === "function") {
660
- el.addEventListener(event, handler);
661
- el.removeAttribute(attr.name);
662
- cleanupListeners.push(() => el.removeEventListener(event, handler));
663
- }
673
+ if (!attr.name.startsWith("@")) continue;
674
+ const event = attr.name.slice(1);
675
+ const handlerName = attr.value;
676
+ const handler = context[handlerName] || TemplateEngine.evaluate(handlerName, context);
677
+ if (typeof handler === "function") {
678
+ el.addEventListener(event, handler);
679
+ el.removeAttribute(attr.name);
680
+ cleanupListeners.push(() => el.removeEventListener(event, handler));
664
681
  }
665
682
  }
666
683
  }
@@ -678,7 +695,6 @@ class Eleva {
678
695
  * @returns {void}
679
696
  */
680
697
  _injectStyles(container, compName, styleFn, context) {
681
- if (!styleFn) return;
682
698
  let styleEl = container.querySelector(`style[data-eleva-style="${compName}"]`);
683
699
  if (!styleEl) {
684
700
  styleEl = document.createElement("style");
@@ -702,6 +718,7 @@ class Eleva {
702
718
  * // Returns: { name: "John", age: "25" }
703
719
  */
704
720
  _extractProps(element, prefix) {
721
+ /** @type {Record<string, string>} */
705
722
  const props = {};
706
723
  for (const {
707
724
  name,
@@ -714,41 +731,6 @@ class Eleva {
714
731
  return props;
715
732
  }
716
733
 
717
- /**
718
- * Mounts a single component instance to a container element.
719
- * This method handles the actual mounting of a component with its props.
720
- *
721
- * @private
722
- * @param {HTMLElement} container - The container element to mount the component to
723
- * @param {string|ComponentDefinition} component - The component to mount, either as a name or definition
724
- * @param {Object<string, any>} props - The props to pass to the component
725
- * @returns {Promise<MountResult>} A promise that resolves to the mounted component instance
726
- * @throws {Error} If the container is not a valid HTMLElement
727
- */
728
- async _mountComponentInstance(container, component, props) {
729
- if (!(container instanceof HTMLElement)) return null;
730
- return await this.mount(container, component, props);
731
- }
732
-
733
- /**
734
- * Mounts components found by a selector in the container.
735
- * This method handles mounting multiple instances of the same component type.
736
- *
737
- * @private
738
- * @param {HTMLElement} container - The container to search for components
739
- * @param {string} selector - The CSS selector to find components
740
- * @param {string|ComponentDefinition} component - The component to mount
741
- * @param {Array<MountResult>} instances - Array to store the mounted component instances
742
- * @returns {Promise<void>}
743
- */
744
- async _mountComponentsBySelector(container, selector, component, instances) {
745
- for (const el of container.querySelectorAll(selector)) {
746
- const props = this._extractProps(el, ":");
747
- const instance = await this._mountComponentInstance(el, component, props);
748
- if (instance) instances.push(instance);
749
- }
750
- }
751
-
752
734
  /**
753
735
  * Mounts all components within the parent component's container.
754
736
  * This method handles mounting of explicitly defined children components.
@@ -771,15 +753,15 @@ class Eleva {
771
753
  * };
772
754
  */
773
755
  async _mountComponents(container, children, childInstances) {
774
- // Clean up existing instances
775
- for (const child of childInstances) child.unmount();
776
- childInstances.length = 0;
777
-
778
- // Mount explicitly defined children components
779
- if (children) {
780
- for (const [selector, component] of Object.entries(children)) {
781
- if (!selector) continue;
782
- await this._mountComponentsBySelector(container, selector, component, childInstances);
756
+ for (const [selector, component] of Object.entries(children)) {
757
+ if (!selector) continue;
758
+ for (const el of container.querySelectorAll(selector)) {
759
+ if (!(el instanceof HTMLElement)) continue;
760
+ const props = this._extractProps(el, ":");
761
+ const instance = await this.mount(el, component, props);
762
+ if (instance && !childInstances.includes(instance)) {
763
+ childInstances.push(instance);
764
+ }
783
765
  }
784
766
  }
785
767
  }