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/README.md +7 -4
- package/dist/eleva.cjs.js +62 -80
- package/dist/eleva.cjs.js.map +1 -1
- package/dist/eleva.d.ts +11 -34
- package/dist/eleva.esm.js +62 -80
- package/dist/eleva.esm.js.map +1 -1
- package/dist/eleva.umd.js +62 -80
- package/dist/eleva.umd.js.map +1 -1
- package/dist/eleva.umd.min.js +3 -0
- package/dist/eleva.umd.min.js.map +1 -0
- package/package.json +98 -75
- package/src/core/Eleva.js +44 -71
- package/src/modules/Emitter.js +1 -1
- package/src/modules/Renderer.js +23 -21
- package/src/modules/Signal.js +1 -0
- package/types/core/Eleva.d.ts +2 -26
- package/types/core/Eleva.d.ts.map +1 -1
- package/types/modules/Emitter.d.ts +2 -2
- package/types/modules/Emitter.d.ts.map +1 -1
- package/types/modules/Renderer.d.ts +5 -5
- package/types/modules/Renderer.d.ts.map +1 -1
- package/types/modules/Signal.d.ts +2 -1
- package/types/modules/Signal.d.ts.map +1 -1
- package/dist/eleva.min.js +0 -3
- package/dist/eleva.min.js.map +0 -1
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():
|
|
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): () =>
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
|
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.
|
|
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():
|
|
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
|
-
*
|
|
240
|
-
*
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 (
|
|
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
|
|
575
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
}
|