eleva 1.2.2-alpha → 1.2.4-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 CHANGED
@@ -1,33 +1,41 @@
1
- # Eleva 🚀
1
+ # eleva.js 🚀
2
2
 
3
3
  Pure JavaScript, Pure Performance, Simply Elegant.
4
4
 
5
5
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![GitHub package.json version](https://img.shields.io/github/package-json/v/tarekraafat/eleva?label=github)](https://github.com/TarekRaafat/eleva)
6
7
  [![Version](https://img.shields.io/npm/v/eleva.svg?style=flat)](https://www.npmjs.com/package/eleva)
7
8
  ![100% Javascript](https://img.shields.io/github/languages/top/TarekRaafat/eleva?color=yellow)
8
- ![Zero Dependencies](https://img.shields.io/badge/Dependencies-0-green.svg)
9
+ ![Zero Dependencies](https://img.shields.io/badge/dependencies-0-green.svg)
10
+ [![codecov](https://codecov.io/gh/TarekRaafat/eleva/branch/master/graph/badge.svg?token=LFA6KENM24)](https://codecov.io/gh/TarekRaafat/eleva)
9
11
  [![Minified Size](https://badgen.net/bundlephobia/min/eleva@latest)](https://bundlephobia.com/package/eleva@latest)
10
12
  [![Gzipped Size](https://badgen.net/bundlephobia/minzip/eleva@latest)](https://bundlephobia.com/package/eleva@latest)
13
+ [![](https://data.jsdelivr.com/v1/package/npm/eleva/badge)](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.js Full Logo.png" alt="eleva.js Full 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.js - A&#0032;minimalist&#0044;&#0032;pure&#0032;vanilla&#0032;javascript&#0032;frontend&#0032;framework&#0046; | 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
- <!-- [![](https://data.jsdelivr.com/v1/package/npm/eleva/badge)](https://www.jsdelivr.com/package/npm/eleva) -->
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 🚀](#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 library.**
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 library lightweight, efficient, and easy to understand.
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 projects needs.
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 whats needed.
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 ~4 KB minified with built-in TypeScript declarations, to keep your bundle lean and your codebase strongly typed.
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 dont require the overhead of a full-fledged framework.
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, Ill follow semantic versioning strictly to clearly communicate any breaking changes.
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 ~4 KB minified and ~1.8 KB gzipped.
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 Elevas efficiency compared to popular frameworks:
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) | **1.8** | **10** | **0.018** | **0.25** | **3.02 (Best)** |
177
- | **React** (Virtual DOM) | 42 | 40 | 0.020 | 0.25 | 20.57 |
178
- | **Vue** (Reactive State) | 33 | 35 | 0.021 | 3.10 | 17.78 |
179
- | **Angular** (Two-way Binding) | 80 | 100 | 0.021 | 0.25 | 45.07 (Slowest) |
184
+ | **Eleva** (Direct DOM) | **2** | **0.05** | **0.002** | **0.25** | **0.58 (Best)** |
185
+ | **React** (Virtual DOM) | 4.1 | 5.34 | 0.020 | 0.25 | 9.71 |
186
+ | **Vue** (Reactive State) | 45 | 4.72 | 0.021 | 3.10 | 13.21 |
187
+ | **Angular** (Two-way Binding) | 62 | 5.26 | 0.021 | 0.25 | 16.88 (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 projects needs.
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 isnt trying to replace these giants but provides a lightweight option when you want simplicity and speed. 🌟
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! Heres how to get started locally:
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
- Id 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.
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. Lets build something great together! 💪✨
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
- const value = this.evaluate(expr, data);
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 keys = Object.keys(data);
32
- const values = Object.values(data);
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._watchers.forEach(fn => fn(newVal));
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
- tempContainer.innerHTML = newHtml;
162
- this.diff(container, tempContainer);
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.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
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.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
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.nodeType === Node.TEXT_NODE) {
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.nodeType === Node.ELEMENT_NODE) {
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
- const attributeToPropertyMap = {
230
- value: "value",
231
- checked: "checked",
232
- selected: "selected",
233
- disabled: "disabled"
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
- // Remove old attributes that no longer exist.
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
- // Add or update attributes from newEl.
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
- oldEl.setAttribute(attr.name, attr.value);
248
- if (attributeToPropertyMap[attr.name]) {
249
- oldEl[attributeToPropertyMap[attr.name]] = attr.value;
250
- } else if (attr.name in oldEl) {
251
- oldEl[attr.name] = attr.value;
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.slice("eleva-prop-".length)] = value;
623
+ props[name.replace("eleva-prop-", "")] = value;
537
624
  }
538
625
  });
539
626
  const instance = this.mount(childEl, children[childSelector], props);