eleva 1.2.2-alpha → 1.2.3-alpha
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 +35 -27
- package/dist/eleva.d.ts +2 -0
- package/dist/eleva.esm.js +122 -35
- package/dist/eleva.esm.js.map +1 -1
- package/dist/eleva.min.js +1 -1
- package/dist/eleva.min.js.map +1 -1
- package/dist/eleva.umd.js +122 -35
- package/dist/eleva.umd.js.map +1 -1
- package/package.json +126 -21
- package/src/core/Eleva.js +9 -4
- package/src/modules/Renderer.js +105 -26
- package/src/modules/Signal.js +21 -1
- package/src/modules/TemplateEngine.js +7 -6
- package/types/core/Eleva.d.ts +2 -0
- package/types/core/Eleva.d.ts.map +1 -1
- package/types/modules/Renderer.d.ts +3 -0
- package/types/modules/Renderer.d.ts.map +1 -1
- package/types/modules/Signal.d.ts +11 -0
- package/types/modules/Signal.d.ts.map +1 -1
- package/types/modules/TemplateEngine.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
|
-
# Eleva 🚀
|
|
1
|
+
# Eleva.js 🚀
|
|
2
2
|
|
|
3
3
|
Pure JavaScript, Pure Performance, Simply Elegant.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/TarekRaafat/eleva)
|
|
6
7
|
[](https://www.npmjs.com/package/eleva)
|
|
7
8
|

|
|
8
|
-

|
|
10
|
+
[](https://codecov.io/gh/TarekRaafat/eleva)
|
|
9
11
|
[](https://bundlephobia.com/package/eleva@latest)
|
|
10
12
|
[](https://bundlephobia.com/package/eleva@latest)
|
|
13
|
+
[](https://www.jsdelivr.com/package/npm/eleva)
|
|
11
14
|
|
|
12
15
|
<br>
|
|
13
16
|
<br>
|
|
14
17
|
|
|
15
18
|
<p align="center">
|
|
16
|
-
<img src="./docs/imgs/Eleva Logo.png" alt="Eleva Logo" width="50%">
|
|
19
|
+
<a href="https://tarekraafat.github.io/eleva/"><img src="./docs/imgs/Eleva Logo.png" alt="Eleva Logo" width="50%"></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://www.producthunt.com/posts/eleva?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-eleva" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=938663&theme=light&t=1741247713068" alt="Eleva - A minimalist, pure vanilla javascript frontend framework. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
17
24
|
</p>
|
|
18
25
|
|
|
26
|
+
<br>
|
|
19
27
|
<br>
|
|
20
28
|
<br>
|
|
21
29
|
|
|
22
30
|
**A minimalist, lightweight, pure vanilla JavaScript frontend runtime framework.**
|
|
23
31
|
_Built with love for native JavaScript—because sometimes, less really is more!_ 😊
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
> **Stability Notice**: This is `v1.2.3-alpha` - The core functionality is stable, but I'm seeking community feedback before the final v1.0.0 release.
|
|
34
|
+
> While suitable for production use, please be aware of the [known limitations](docs/known-limitations.md).
|
|
35
|
+
|
|
36
|
+
**Version:** `1.2.3-alpha`
|
|
26
37
|
|
|
27
|
-
> **Stability Notice**: This is `v1.2.0-alpha` - APIs may change significantly until the stable release.
|
|
28
|
-
> Not recommended for production use yet. Follow the [versioning guide](#version-guide) for updates.
|
|
29
38
|
|
|
30
|
-
**Version:** `1.2.0-alpha`
|
|
31
39
|
|
|
32
40
|
Welcome to Eleva! This is my humble, experimental playground for a fresh approach to frontend development. Eleva was born out of my genuine passion for pure vanilla JavaScript—no frameworks, no bloat, just the power of native code. I hope you'll have fun exploring, testing, and contributing to make Eleva even better. 🚀
|
|
33
41
|
|
|
@@ -35,7 +43,7 @@ Welcome to Eleva! This is my humble, experimental playground for a fresh approac
|
|
|
35
43
|
|
|
36
44
|
## Table of Contents
|
|
37
45
|
|
|
38
|
-
- [Eleva 🚀](#
|
|
46
|
+
- [Eleva.js 🚀](#elevajs-)
|
|
39
47
|
- [Table of Contents](#table-of-contents)
|
|
40
48
|
- [Introduction](#introduction)
|
|
41
49
|
- [Design Philosophy](#design-philosophy)
|
|
@@ -67,7 +75,7 @@ Welcome to Eleva! This is my humble, experimental playground for a fresh approac
|
|
|
67
75
|
|
|
68
76
|
## Introduction
|
|
69
77
|
|
|
70
|
-
Eleva is a lightweight, no-nonsense runtime framework for frontend applications. Built with love for **pure vanilla JavaScript**, Eleva lets you create highly modular and scalable applications without the overhead of large frameworks. I built Eleva to prove that you don't need heavy libraries to build amazing user interfaces—sometimes, the simplest approach is the most powerful.
|
|
78
|
+
Eleva is a lightweight, no-nonsense runtime framework for frontend applications. Built with love for **pure vanilla JavaScript**, Eleva lets you create highly modular and scalable applications without the overhead of large frameworks. I built Eleva to prove that you don't need heavy frameworks or libraries to build amazing user interfaces—sometimes, the simplest approach is the most powerful.
|
|
71
79
|
|
|
72
80
|
**My Inspiration:**
|
|
73
81
|
The idea behind Eleva comes from a deep appreciation for native JavaScript. I wanted to create a tool that stays true to the language without introducing new syntax or complexity, making it easy to integrate into your projects.
|
|
@@ -83,7 +91,7 @@ The idea behind Eleva comes from a deep appreciation for native JavaScript. I wa
|
|
|
83
91
|
|
|
84
92
|
## Design Philosophy
|
|
85
93
|
|
|
86
|
-
**Eleva is an unopinionated
|
|
94
|
+
**Eleva is an unopinionated framework.**
|
|
87
95
|
|
|
88
96
|
Unlike many frameworks that enforce a specific project structure or coding paradigm, Eleva provides a minimal core with a flexible plugin system. This means:
|
|
89
97
|
|
|
@@ -100,10 +108,10 @@ This unopinionated approach makes Eleva versatile and ideal for developers who w
|
|
|
100
108
|
|
|
101
109
|
Eleva is built with meticulous attention to detail and a deep passion for pure vanilla JavaScript. Every aspect of its design and architecture is handcrafted with the developer in mind. This makes Eleva not only innovative but also a solid foundation for your projects.
|
|
102
110
|
|
|
103
|
-
- **🎨 Craftsmanship:** Every line of code is written with care, keeping the
|
|
111
|
+
- **🎨 Craftsmanship:** Every line of code is written with care, keeping the framework lightweight, efficient, and easy to understand.
|
|
104
112
|
- **🛠️ Developer-Centric:** Its intuitive API and minimal core mean you spend less time wrestling with the framework and more time building your application.
|
|
105
113
|
- **🌟 Innovative & Fresh:** Stick to pure vanilla JavaScript and avoid unnecessary abstractions.
|
|
106
|
-
- **🏗️ Solid & Reliable:** Focused on performance and modularity, Eleva scales with your project
|
|
114
|
+
- **🏗️ Solid & Reliable:** Focused on performance and modularity, Eleva scales with your project's needs.
|
|
107
115
|
|
|
108
116
|
This unique, developer-first approach makes Eleva a standout choice for building high-performance frontend applications without compromising on simplicity or control.
|
|
109
117
|
|
|
@@ -112,13 +120,13 @@ This unique, developer-first approach makes Eleva a standout choice for building
|
|
|
112
120
|
## Features
|
|
113
121
|
|
|
114
122
|
- **🧩 Component-Based Architecture:** Create reusable UI components effortlessly.
|
|
115
|
-
- **⚡ Signal-Based Reactivity:** Fine-grained reactivity that updates only what
|
|
123
|
+
- **⚡ Signal-Based Reactivity:** Fine-grained reactivity that updates only what's needed.
|
|
116
124
|
- **🔔 Event Handling:** Built-in event emitter for robust inter-component communication.
|
|
117
125
|
- **📝 Template Parsing:** Secure and dynamic interpolation with a custom TemplateEngine.
|
|
118
126
|
- **🔄 DOM Diffing & Patching:** High-performance updates without a virtual DOM.
|
|
119
127
|
- **📦 UMD & ES Module Builds:** Supports modern build tools and browser environments.
|
|
120
128
|
- **🤝 Friendly API:** A gentle learning curve for both beginners and seasoned developers.
|
|
121
|
-
- **💎 Tiny Footprint & TypeScript Support:** Approximately ~
|
|
129
|
+
- **💎 Tiny Footprint & TypeScript Support:** Approximately ~6 KB minified with built-in TypeScript declarations, to keep your bundle lean and your codebase strongly typed.
|
|
122
130
|
|
|
123
131
|
---
|
|
124
132
|
|
|
@@ -126,7 +134,7 @@ This unique, developer-first approach makes Eleva a standout choice for building
|
|
|
126
134
|
|
|
127
135
|
Eleva is ideal for developers seeking a lightweight, flexible, and high-performance solution for building frontend applications. Here are some scenarios where Eleva shines:
|
|
128
136
|
|
|
129
|
-
- **🚀 Small to Medium Projects:** Perfect for web apps or websites that don
|
|
137
|
+
- **🚀 Small to Medium Projects:** Perfect for web apps or websites that don't require the overhead of a full-fledged framework.
|
|
130
138
|
- **⚡ Performance-Critical Applications:** Optimized reactivity and DOM diffing ensure smooth performance without bloat.
|
|
131
139
|
- **🔄 Unopinionated & Flexible:** Architect your application your way with a straightforward API and plugin system.
|
|
132
140
|
- **🎯 Developer-Friendly:** Stick to pure vanilla JavaScript with familiar syntax and built-in TypeScript support.
|
|
@@ -140,7 +148,7 @@ Eleva is ideal for developers seeking a lightweight, flexible, and high-performa
|
|
|
140
148
|
I believe in clear versioning that reflects the maturity of the project:
|
|
141
149
|
|
|
142
150
|
- **Pre-release Versions (Alpha/Beta):** Early versions like `1.2.0-alpha` indicate the API is still evolving. Expect frequent updates and share your feedback!
|
|
143
|
-
- **Semantic Versioning:** Once stable, I
|
|
151
|
+
- **Semantic Versioning:** Once stable, I'll follow semantic versioning strictly to clearly communicate any breaking changes.
|
|
144
152
|
- **Fresh Start:** This release (`1.2.0-alpha`) marks a significant update with enhanced inline documentation, improved JSDoc annotations, and a refined mounting context that now includes an `emitter` property.
|
|
145
153
|
|
|
146
154
|
---
|
|
@@ -160,7 +168,7 @@ I follow [Semantic Versioning (SemVer)](https://semver.org/):
|
|
|
160
168
|
|
|
161
169
|
Eleva is crafted for performance:
|
|
162
170
|
|
|
163
|
-
- **Lightweight:** Approximately ~
|
|
171
|
+
- **Lightweight:** Approximately ~6 KB minified and ~2 KB gzipped.
|
|
164
172
|
- **Efficient Reactivity:** Signal-based updates ensure only necessary DOM parts are updated.
|
|
165
173
|
- **Optimized Diffing:** Renderer efficiently patches changes without the overhead of a virtual DOM.
|
|
166
174
|
- **No Bloat:** Pure vanilla JavaScript with zero dependencies keeps your project nimble.
|
|
@@ -169,14 +177,14 @@ Eleva is crafted for performance:
|
|
|
169
177
|
|
|
170
178
|
## Performance Benchmarks
|
|
171
179
|
|
|
172
|
-
Preliminary benchmarks illustrate Eleva
|
|
180
|
+
Preliminary benchmarks illustrate Eleva's efficiency compared to popular frameworks:
|
|
173
181
|
|
|
174
182
|
| **Framework** | **Bundle Size** (KB) | **Initial Load Time** (ms) | **DOM Update Speed** (s) | **Peak Memory Usage** (KB) | **Overall Performance Score** (lower is better) |
|
|
175
183
|
| ----------------------------- | -------------------- | -------------------------- | ------------------------ | -------------------------- | ----------------------------------------------- |
|
|
176
|
-
| **Eleva** (Direct DOM) | **
|
|
177
|
-
| **React** (Virtual DOM) | 42 |
|
|
178
|
-
| **Vue** (Reactive State) | 33 |
|
|
179
|
-
| **Angular** (Two-way Binding) | 80 |
|
|
184
|
+
| **Eleva** (Direct DOM) | **2** | **0.05** | **0.002** | **0.25** | **0.58 (Best)** |
|
|
185
|
+
| **React** (Virtual DOM) | 42 | 5.34 | 0.020 | 0.25 | 20.57 |
|
|
186
|
+
| **Vue** (Reactive State) | 33 | 4.72 | 0.021 | 3.10 | 17.78 |
|
|
187
|
+
| **Angular** (Two-way Binding) | 80 | 5.26 | 0.021 | 0.25 | 45.07 (Slowest) |
|
|
180
188
|
|
|
181
189
|
Detailed [Benchmark Metrics Report](BENCHMARK.md)
|
|
182
190
|
|
|
@@ -189,11 +197,11 @@ Detailed [Benchmark Metrics Report](BENCHMARK.md)
|
|
|
189
197
|
Eleva offers a refreshing alternative to frameworks like React, Vue, and Angular:
|
|
190
198
|
|
|
191
199
|
- **Simplicity:** No virtual DOM, JSX, or complex state management—just plain JavaScript.
|
|
192
|
-
- **Modularity:** Easily extend via plugins to suit your project
|
|
200
|
+
- **Modularity:** Easily extend via plugins to suit your project's needs.
|
|
193
201
|
- **Size:** A fraction of the size of mainstream frameworks.
|
|
194
202
|
- **Learning Curve:** Familiar syntax and a clear API make it accessible to all developers.
|
|
195
203
|
|
|
196
|
-
_Note:_ Eleva isn
|
|
204
|
+
_Note:_ Eleva isn't trying to replace these giants but provides a lightweight option when you want simplicity and speed. 🌟
|
|
197
205
|
|
|
198
206
|
---
|
|
199
207
|
|
|
@@ -347,7 +355,7 @@ For detailed API documentation, please check the [docs](docs/index.md) folder.
|
|
|
347
355
|
|
|
348
356
|
## Development
|
|
349
357
|
|
|
350
|
-
I welcome developers to dive in and experiment with Eleva! Here
|
|
358
|
+
I welcome developers to dive in and experiment with Eleva! Here's how to get started locally:
|
|
351
359
|
|
|
352
360
|
1. **Clone the Repository:**
|
|
353
361
|
|
|
@@ -403,7 +411,7 @@ Contributions to tests are very welcome! 🧪
|
|
|
403
411
|
|
|
404
412
|
## Contributing
|
|
405
413
|
|
|
406
|
-
I
|
|
414
|
+
I'd love to have you onboard as a contributor! Whether you're adding new features, squashing bugs, or sharing ideas, your input is invaluable. Please check out [CONTRIBUTING](CONTRIBUTING.md) for guidelines on getting started.
|
|
407
415
|
|
|
408
416
|
---
|
|
409
417
|
|
|
@@ -421,7 +429,7 @@ Eleva is open-source and available under the [MIT License](LICENSE).
|
|
|
421
429
|
|
|
422
430
|
---
|
|
423
431
|
|
|
424
|
-
**Note:** This is an alpha release. I'm still polishing things up, so expect some bumps along the way. Your feedback and contributions will help shape Eleva into something truly amazing. Let
|
|
432
|
+
**Note:** This is an alpha release. I'm still polishing things up, so expect some bumps along the way. Your feedback and contributions will help shape Eleva into something truly amazing. Let's build something great together! 💪✨
|
|
425
433
|
|
|
426
434
|
---
|
|
427
435
|
|
package/dist/eleva.d.ts
CHANGED
|
@@ -95,9 +95,11 @@ declare class Eleva {
|
|
|
95
95
|
private _prepareLifecycleHooks;
|
|
96
96
|
/**
|
|
97
97
|
* Processes DOM elements for event binding based on attributes starting with "@".
|
|
98
|
+
* Tracks listeners for cleanup during unmount.
|
|
98
99
|
*
|
|
99
100
|
* @param {HTMLElement} container - The container element in which to search for events.
|
|
100
101
|
* @param {Object<string, any>} context - The current context containing event handler definitions.
|
|
102
|
+
* @param {Array<Function>} cleanupListeners - Array to collect cleanup functions for each event listener.
|
|
101
103
|
* @private
|
|
102
104
|
*/
|
|
103
105
|
private _processEvents;
|
package/dist/eleva.esm.js
CHANGED
|
@@ -13,9 +13,9 @@ class TemplateEngine {
|
|
|
13
13
|
* @returns {string} The resulting string with evaluated values.
|
|
14
14
|
*/
|
|
15
15
|
static parse(template, data) {
|
|
16
|
+
if (!template || typeof template !== "string") return template;
|
|
16
17
|
return template.replace(/\{\{\s*(.*?)\s*\}\}/g, (_, expr) => {
|
|
17
|
-
|
|
18
|
-
return value === undefined ? "" : value;
|
|
18
|
+
return this.evaluate(expr, data);
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -27,11 +27,10 @@ class TemplateEngine {
|
|
|
27
27
|
* @returns {any} The result of the evaluated expression, or an empty string if undefined or on error.
|
|
28
28
|
*/
|
|
29
29
|
static evaluate(expr, data) {
|
|
30
|
+
if (!expr || typeof expr !== "string") return expr;
|
|
30
31
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const result = new Function(...keys, `return ${expr}`)(...values);
|
|
34
|
-
return result === undefined ? "" : result;
|
|
32
|
+
const compiledFn = new Function("data", `with(data) { return ${expr} }`);
|
|
33
|
+
return compiledFn(data);
|
|
35
34
|
} catch (error) {
|
|
36
35
|
console.error(`Template evaluation error:`, {
|
|
37
36
|
expression: expr,
|
|
@@ -60,6 +59,8 @@ class Signal {
|
|
|
60
59
|
this._value = value;
|
|
61
60
|
/** @private {Set<function>} Collection of callback functions to be notified when value changes */
|
|
62
61
|
this._watchers = new Set();
|
|
62
|
+
/** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
|
|
63
|
+
this._pending = false;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -79,7 +80,7 @@ class Signal {
|
|
|
79
80
|
set value(newVal) {
|
|
80
81
|
if (newVal !== this._value) {
|
|
81
82
|
this._value = newVal;
|
|
82
|
-
this.
|
|
83
|
+
this._notifyWatchers();
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -93,6 +94,24 @@ class Signal {
|
|
|
93
94
|
this._watchers.add(fn);
|
|
94
95
|
return () => this._watchers.delete(fn);
|
|
95
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Notifies all registered watchers of a value change using microtask scheduling.
|
|
100
|
+
* Uses a pending flag to batch multiple synchronous updates into a single notification.
|
|
101
|
+
* All watcher callbacks receive the current value when executed.
|
|
102
|
+
*
|
|
103
|
+
* @private
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
_notifyWatchers() {
|
|
107
|
+
if (!this._pending) {
|
|
108
|
+
this._pending = true;
|
|
109
|
+
queueMicrotask(() => {
|
|
110
|
+
this._pending = false;
|
|
111
|
+
this._watchers.forEach(fn => fn(this._value));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
/**
|
|
@@ -155,11 +174,24 @@ class Renderer {
|
|
|
155
174
|
*
|
|
156
175
|
* @param {HTMLElement} container - The container element to patch.
|
|
157
176
|
* @param {string} newHtml - The new HTML content to apply.
|
|
177
|
+
* @throws {Error} If container is not an HTMLElement or newHtml is not a string
|
|
158
178
|
*/
|
|
159
179
|
patchDOM(container, newHtml) {
|
|
180
|
+
if (!(container instanceof HTMLElement)) {
|
|
181
|
+
throw new Error("Container must be an HTMLElement");
|
|
182
|
+
}
|
|
183
|
+
if (typeof newHtml !== "string") {
|
|
184
|
+
throw new Error("newHtml must be a string");
|
|
185
|
+
}
|
|
160
186
|
const tempContainer = document.createElement("div");
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
try {
|
|
188
|
+
tempContainer.innerHTML = newHtml;
|
|
189
|
+
this.diff(container, tempContainer);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new Error(`Failed to patch DOM: ${error.message}`);
|
|
192
|
+
} finally {
|
|
193
|
+
tempContainer.innerHTML = "";
|
|
194
|
+
}
|
|
163
195
|
}
|
|
164
196
|
|
|
165
197
|
/**
|
|
@@ -167,56 +199,71 @@ class Renderer {
|
|
|
167
199
|
*
|
|
168
200
|
* @param {HTMLElement} oldParent - The original DOM element.
|
|
169
201
|
* @param {HTMLElement} newParent - The new DOM element.
|
|
202
|
+
* @throws {Error} If either parent is not an HTMLElement
|
|
170
203
|
*/
|
|
171
204
|
diff(oldParent, newParent) {
|
|
205
|
+
if (!(oldParent instanceof HTMLElement) || !(newParent instanceof HTMLElement)) {
|
|
206
|
+
throw new Error("Both parents must be HTMLElements");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fast path for identical nodes
|
|
210
|
+
if (oldParent.isEqualNode(newParent)) return;
|
|
172
211
|
const oldNodes = Array.from(oldParent.childNodes);
|
|
173
212
|
const newNodes = Array.from(newParent.childNodes);
|
|
174
213
|
const max = Math.max(oldNodes.length, newNodes.length);
|
|
214
|
+
|
|
215
|
+
// Batch DOM operations for better performance
|
|
216
|
+
const operations = [];
|
|
175
217
|
for (let i = 0; i < max; i++) {
|
|
176
218
|
const oldNode = oldNodes[i];
|
|
177
219
|
const newNode = newNodes[i];
|
|
178
220
|
|
|
179
221
|
// Case 1: Append new nodes that don't exist in the old tree.
|
|
180
222
|
if (!oldNode && newNode) {
|
|
181
|
-
oldParent.appendChild(newNode.cloneNode(true));
|
|
223
|
+
operations.push(() => oldParent.appendChild(newNode.cloneNode(true)));
|
|
182
224
|
continue;
|
|
183
225
|
}
|
|
184
226
|
// Case 2: Remove old nodes not present in the new tree.
|
|
185
227
|
if (oldNode && !newNode) {
|
|
186
|
-
oldParent.removeChild(oldNode);
|
|
228
|
+
operations.push(() => oldParent.removeChild(oldNode));
|
|
187
229
|
continue;
|
|
188
230
|
}
|
|
189
231
|
|
|
190
232
|
// Case 3: For element nodes, compare keys if available.
|
|
191
|
-
if (oldNode
|
|
233
|
+
if (oldNode?.nodeType === Node.ELEMENT_NODE && newNode?.nodeType === Node.ELEMENT_NODE) {
|
|
192
234
|
const oldKey = oldNode.getAttribute("key");
|
|
193
235
|
const newKey = newNode.getAttribute("key");
|
|
194
236
|
if (oldKey || newKey) {
|
|
195
237
|
if (oldKey !== newKey) {
|
|
196
|
-
oldParent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
238
|
+
operations.push(() => oldParent.replaceChild(newNode.cloneNode(true), oldNode));
|
|
197
239
|
continue;
|
|
198
240
|
}
|
|
199
241
|
}
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
// Case 4: Replace nodes if types or tag names differ.
|
|
203
|
-
if (oldNode
|
|
204
|
-
oldParent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
245
|
+
if (oldNode?.nodeType !== newNode?.nodeType || oldNode?.nodeName !== newNode?.nodeName) {
|
|
246
|
+
operations.push(() => oldParent.replaceChild(newNode.cloneNode(true), oldNode));
|
|
205
247
|
continue;
|
|
206
248
|
}
|
|
249
|
+
|
|
207
250
|
// Case 5: For text nodes, update content if different.
|
|
208
|
-
if (oldNode
|
|
251
|
+
if (oldNode?.nodeType === Node.TEXT_NODE) {
|
|
209
252
|
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
210
253
|
oldNode.nodeValue = newNode.nodeValue;
|
|
211
254
|
}
|
|
212
255
|
continue;
|
|
213
256
|
}
|
|
257
|
+
|
|
214
258
|
// Case 6: For element nodes, update attributes and then diff children.
|
|
215
|
-
if (oldNode
|
|
259
|
+
if (oldNode?.nodeType === Node.ELEMENT_NODE) {
|
|
216
260
|
this.updateAttributes(oldNode, newNode);
|
|
217
261
|
this.diff(oldNode, newNode);
|
|
218
262
|
}
|
|
219
263
|
}
|
|
264
|
+
|
|
265
|
+
// Execute batched operations
|
|
266
|
+
operations.forEach(op => op());
|
|
220
267
|
}
|
|
221
268
|
|
|
222
269
|
/**
|
|
@@ -224,34 +271,69 @@ class Renderer {
|
|
|
224
271
|
*
|
|
225
272
|
* @param {HTMLElement} oldEl - The element to update.
|
|
226
273
|
* @param {HTMLElement} newEl - The element providing the updated attributes.
|
|
274
|
+
* @throws {Error} If either element is not an HTMLElement
|
|
227
275
|
*/
|
|
228
276
|
updateAttributes(oldEl, newEl) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
277
|
+
if (!(oldEl instanceof HTMLElement) || !(newEl instanceof HTMLElement)) {
|
|
278
|
+
throw new Error("Both elements must be HTMLElements");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Special cases for properties that don't map directly to attributes
|
|
282
|
+
const specialProperties = {
|
|
283
|
+
value: true,
|
|
284
|
+
checked: true,
|
|
285
|
+
selected: true,
|
|
286
|
+
disabled: true,
|
|
287
|
+
readOnly: true,
|
|
288
|
+
multiple: true
|
|
234
289
|
};
|
|
235
290
|
|
|
236
|
-
//
|
|
291
|
+
// Batch attribute operations for better performance
|
|
292
|
+
const operations = [];
|
|
293
|
+
|
|
294
|
+
// Remove old attributes that no longer exist
|
|
237
295
|
Array.from(oldEl.attributes).forEach(attr => {
|
|
238
296
|
if (attr.name.startsWith("@")) return;
|
|
239
297
|
if (!newEl.hasAttribute(attr.name)) {
|
|
240
|
-
oldEl.removeAttribute(attr.name);
|
|
298
|
+
operations.push(() => oldEl.removeAttribute(attr.name));
|
|
241
299
|
}
|
|
242
300
|
});
|
|
243
|
-
|
|
301
|
+
|
|
302
|
+
// Add or update attributes from newEl
|
|
244
303
|
Array.from(newEl.attributes).forEach(attr => {
|
|
245
304
|
if (attr.name.startsWith("@")) return;
|
|
246
305
|
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
306
|
+
operations.push(() => {
|
|
307
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
308
|
+
|
|
309
|
+
// Convert kebab-case to camelCase for property names
|
|
310
|
+
const propName = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
311
|
+
|
|
312
|
+
// Handle special cases first
|
|
313
|
+
if (specialProperties[propName]) {
|
|
314
|
+
oldEl[propName] = attr.value === "" ? true : attr.value;
|
|
315
|
+
}
|
|
316
|
+
// Handle ARIA attributes
|
|
317
|
+
else if (attr.name.startsWith("aria-")) {
|
|
318
|
+
const ariaName = "aria" + attr.name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
319
|
+
oldEl[ariaName] = attr.value;
|
|
320
|
+
}
|
|
321
|
+
// Handle data attributes
|
|
322
|
+
else if (attr.name.startsWith("data-")) {
|
|
323
|
+
// dataset handles the camelCase conversion automatically
|
|
324
|
+
const dataName = attr.name.slice(5);
|
|
325
|
+
oldEl.dataset[dataName] = attr.value;
|
|
326
|
+
}
|
|
327
|
+
// Handle standard properties
|
|
328
|
+
else if (propName in oldEl) {
|
|
329
|
+
oldEl[propName] = attr.value;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
253
332
|
}
|
|
254
333
|
});
|
|
334
|
+
|
|
335
|
+
// Execute batched operations
|
|
336
|
+
operations.forEach(op => op());
|
|
255
337
|
}
|
|
256
338
|
}
|
|
257
339
|
|
|
@@ -401,6 +483,7 @@ class Eleva {
|
|
|
401
483
|
};
|
|
402
484
|
const watcherUnsubscribers = [];
|
|
403
485
|
const childInstances = [];
|
|
486
|
+
const cleanupListeners = [];
|
|
404
487
|
if (!this._isMounted) {
|
|
405
488
|
mergedContext.onBeforeMount && mergedContext.onBeforeMount();
|
|
406
489
|
} else {
|
|
@@ -414,7 +497,7 @@ class Eleva {
|
|
|
414
497
|
const render = () => {
|
|
415
498
|
const newHtml = TemplateEngine.parse(template(mergedContext), mergedContext);
|
|
416
499
|
this.renderer.patchDOM(container, newHtml);
|
|
417
|
-
this._processEvents(container, mergedContext);
|
|
500
|
+
this._processEvents(container, mergedContext, cleanupListeners);
|
|
418
501
|
this._injectStyles(container, compName, style, mergedContext);
|
|
419
502
|
this._mountChildren(container, children, childInstances);
|
|
420
503
|
if (!this._isMounted) {
|
|
@@ -438,12 +521,13 @@ class Eleva {
|
|
|
438
521
|
container,
|
|
439
522
|
data: mergedContext,
|
|
440
523
|
/**
|
|
441
|
-
* Unmounts the component, cleaning up watchers, child components, and clearing the container.
|
|
524
|
+
* Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
|
|
442
525
|
*
|
|
443
526
|
* @returns {void}
|
|
444
527
|
*/
|
|
445
528
|
unmount: () => {
|
|
446
529
|
watcherUnsubscribers.forEach(fn => fn());
|
|
530
|
+
cleanupListeners.forEach(fn => fn());
|
|
447
531
|
childInstances.forEach(child => child.unmount());
|
|
448
532
|
mergedContext.onUnmount && mergedContext.onUnmount();
|
|
449
533
|
container.innerHTML = "";
|
|
@@ -470,12 +554,14 @@ class Eleva {
|
|
|
470
554
|
|
|
471
555
|
/**
|
|
472
556
|
* Processes DOM elements for event binding based on attributes starting with "@".
|
|
557
|
+
* Tracks listeners for cleanup during unmount.
|
|
473
558
|
*
|
|
474
559
|
* @param {HTMLElement} container - The container element in which to search for events.
|
|
475
560
|
* @param {Object<string, any>} context - The current context containing event handler definitions.
|
|
561
|
+
* @param {Array<Function>} cleanupListeners - Array to collect cleanup functions for each event listener.
|
|
476
562
|
* @private
|
|
477
563
|
*/
|
|
478
|
-
_processEvents(container, context) {
|
|
564
|
+
_processEvents(container, context, cleanupListeners) {
|
|
479
565
|
container.querySelectorAll("*").forEach(el => {
|
|
480
566
|
[...el.attributes].forEach(({
|
|
481
567
|
name,
|
|
@@ -487,6 +573,7 @@ class Eleva {
|
|
|
487
573
|
if (typeof handler === "function") {
|
|
488
574
|
el.addEventListener(event, handler);
|
|
489
575
|
el.removeAttribute(name);
|
|
576
|
+
cleanupListeners.push(() => el.removeEventListener(event, handler));
|
|
490
577
|
}
|
|
491
578
|
}
|
|
492
579
|
});
|
|
@@ -533,7 +620,7 @@ class Eleva {
|
|
|
533
620
|
value
|
|
534
621
|
}) => {
|
|
535
622
|
if (name.startsWith("eleva-prop-")) {
|
|
536
|
-
props[name.
|
|
623
|
+
props[name.replace("eleva-prop-", "")] = value;
|
|
537
624
|
}
|
|
538
625
|
});
|
|
539
626
|
const instance = this.mount(childEl, children[childSelector], props);
|