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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eleva",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3-alpha",
|
|
4
4
|
"description": "A minimalist and lightweight, pure vanilla JavaScript frontend runtime framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/eleva.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"require": "./dist/eleva.js",
|
|
13
|
-
"import": "./dist/eleva.esm.js"
|
|
13
|
+
"import": "./dist/eleva.esm.js",
|
|
14
|
+
"types": "./dist/eleva.d.ts"
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
@@ -18,45 +19,129 @@
|
|
|
18
19
|
"build": "rollup -c",
|
|
19
20
|
"build:types": "tsc --emitDeclarationOnly",
|
|
20
21
|
"build:types:bundle": "rollup -c rollup.dts.config.js",
|
|
21
|
-
"build:all": "npm run build && npm run build:types && npm run build:types:bundle",
|
|
22
|
-
"
|
|
22
|
+
"build:all": "npm run clean && npm run build && npm run build:types && npm run build:types:bundle",
|
|
23
|
+
"docs:dev": "vitepress dev docs",
|
|
24
|
+
"docs:build": "vitepress build docs",
|
|
25
|
+
"docs:preview": "vitepress preview docs",
|
|
26
|
+
"docs:api": "jsdoc -c jsdoc.json",
|
|
27
|
+
"docs:generate": "npm run docs:api && npm run docs:build",
|
|
28
|
+
"test": "npm run test:all",
|
|
29
|
+
"test:source": "jest -c jest.source.config.js",
|
|
30
|
+
"test:source:unit": "jest -c jest.source.config.js --testPathPattern=source/unit",
|
|
31
|
+
"test:source:performance": "jest -c jest.source.config.js --testPathPattern=source/performance",
|
|
32
|
+
"test:source:coverage": "jest -c jest.source.config.js --testPathPattern=source/unit --coverage",
|
|
33
|
+
"test:source:watch": "jest -c jest.source.config.js --watch",
|
|
34
|
+
"test:source:debug": "jest -c jest.source.config.js --runInBand --no-cache --watchAll",
|
|
35
|
+
"test:build": "jest -c jest.build.config.js",
|
|
36
|
+
"test:build:unit": "jest -c jest.build.config.js --testPathPattern=build/unit",
|
|
37
|
+
"test:build:performance": "jest -c jest.build.config.js --testPathPattern=build/performance",
|
|
38
|
+
"test:build:coverage": "jest -c jest.build.config.js --coverage",
|
|
39
|
+
"test:build:watch": "jest -c jest.build.config.js --watch",
|
|
40
|
+
"test:build:debug": "jest -c jest.build.config.js --runInBand --no-cache --watchAll",
|
|
41
|
+
"test:all": "npm run test:source && npm run test:build",
|
|
42
|
+
"codecov": "npx codecov",
|
|
23
43
|
"lint": "eslint src/**/*.js",
|
|
44
|
+
"format": "npx prettier \"src/**/*\" \"types/**/*\" --write",
|
|
24
45
|
"size": "size-limit",
|
|
25
|
-
"
|
|
46
|
+
"clean": "rimraf dist types",
|
|
47
|
+
"prepublishOnly": "npm run format && npm run lint && npm run test:source:coverage && npm run build:all",
|
|
48
|
+
"prepare": "npm run build:all"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=16.0.0"
|
|
26
52
|
},
|
|
27
53
|
"devDependencies": {
|
|
28
|
-
"@babel/core": "^7.
|
|
29
|
-
"@babel/preset-env": "^7.
|
|
54
|
+
"@babel/core": "^7.26.10",
|
|
55
|
+
"@babel/preset-env": "^7.26.9",
|
|
56
|
+
"@codecov/rollup-plugin": "^1.9.0",
|
|
30
57
|
"@rollup/plugin-babel": "^6.0.3",
|
|
31
58
|
"@rollup/plugin-commonjs": "^28.0.2",
|
|
32
59
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
33
60
|
"@rollup/plugin-terser": "^0.4.4",
|
|
34
61
|
"@size-limit/preset-app": "^11.2.0",
|
|
62
|
+
"babel-jest": "^29.7.0",
|
|
35
63
|
"eslint": "^9.0.0",
|
|
64
|
+
"eslint-plugin-prettier": "^5.2.5",
|
|
65
|
+
"husky": "^9.1.7",
|
|
36
66
|
"jest": "^29.6.1",
|
|
67
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
68
|
+
"jest-html-reporters": "^3.1.7",
|
|
69
|
+
"jest-watch-typeahead": "^2.2.2",
|
|
70
|
+
"jsdoc": "^4.0.4",
|
|
71
|
+
"lint-staged": "^15.5.0",
|
|
72
|
+
"prettier": "^3.5.3",
|
|
73
|
+
"rimraf": "^6.0.1",
|
|
37
74
|
"rollup": "^4.34.8",
|
|
38
75
|
"rollup-plugin-dts": "^6.1.1",
|
|
39
76
|
"size-limit": "^11.2.0",
|
|
40
|
-
"typescript": "^5.1.6"
|
|
77
|
+
"typescript": "^5.1.6",
|
|
78
|
+
"vitepress": "^1.6.3"
|
|
79
|
+
},
|
|
80
|
+
"lint-staged": {
|
|
81
|
+
"src/**/*.{js,ts}": [
|
|
82
|
+
"eslint --fix",
|
|
83
|
+
"prettier --write"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"husky": {
|
|
87
|
+
"hooks": {
|
|
88
|
+
"pre-commit": "lint-staged"
|
|
89
|
+
}
|
|
41
90
|
},
|
|
42
91
|
"size-limit": [
|
|
43
92
|
{
|
|
44
93
|
"limit": "5 kB",
|
|
45
94
|
"path": "dist/eleva.min.js",
|
|
46
95
|
"name": "eleva",
|
|
47
|
-
"brotli":
|
|
96
|
+
"brotli": true
|
|
48
97
|
}
|
|
49
98
|
],
|
|
50
99
|
"keywords": [
|
|
51
|
-
"modern",
|
|
52
|
-
"
|
|
53
|
-
"framework",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
100
|
+
"modern javascript framework",
|
|
101
|
+
"lightweight JavaScript framework",
|
|
102
|
+
"lightweight JS framework",
|
|
103
|
+
"frontend performance optimization",
|
|
104
|
+
"frontend performance",
|
|
105
|
+
"performance optimization",
|
|
106
|
+
"tiny footprint",
|
|
107
|
+
"minimal framework",
|
|
108
|
+
"fast framework",
|
|
109
|
+
"highly performant",
|
|
110
|
+
"lightweight framework",
|
|
111
|
+
"frontend optimization",
|
|
112
|
+
"small bundle size",
|
|
113
|
+
"tree-shakable",
|
|
114
|
+
"ui components",
|
|
115
|
+
"vanilla JavaScript",
|
|
116
|
+
"vanilla-js",
|
|
117
|
+
"vanilla framework",
|
|
118
|
+
"vanilla JavaScript framework",
|
|
119
|
+
"signal-based reactivity",
|
|
120
|
+
"reactivity",
|
|
121
|
+
"frontend runtime",
|
|
122
|
+
"runtime framework",
|
|
123
|
+
"web components",
|
|
124
|
+
"javascript framework",
|
|
125
|
+
"frontend framework",
|
|
126
|
+
"direct DOM",
|
|
127
|
+
"high performance",
|
|
128
|
+
"open source",
|
|
129
|
+
"modular",
|
|
130
|
+
"developer friendly",
|
|
131
|
+
"efficient",
|
|
132
|
+
"scalable",
|
|
133
|
+
"pure JavaScript",
|
|
134
|
+
"minimalist framework",
|
|
135
|
+
"high-speed rendering",
|
|
136
|
+
"unopinionated",
|
|
137
|
+
"typescript",
|
|
138
|
+
"typescript support",
|
|
139
|
+
"single-page applications",
|
|
140
|
+
"UI library",
|
|
141
|
+
"web development",
|
|
142
|
+
"web development framework",
|
|
143
|
+
"web development tools",
|
|
144
|
+
"micro frontends"
|
|
60
145
|
],
|
|
61
146
|
"author": {
|
|
62
147
|
"name": "Tarek Raafat",
|
|
@@ -64,7 +149,7 @@
|
|
|
64
149
|
"url": "https://www.tarekraafat.com"
|
|
65
150
|
},
|
|
66
151
|
"license": "MIT",
|
|
67
|
-
"homepage": "https://
|
|
152
|
+
"homepage": "https://elevajs.com",
|
|
68
153
|
"repository": {
|
|
69
154
|
"type": "git",
|
|
70
155
|
"url": "git+https://github.com/TarekRaafat/eleva.git"
|
|
@@ -76,12 +161,32 @@
|
|
|
76
161
|
"directories": {
|
|
77
162
|
"doc": "docs",
|
|
78
163
|
"example": "examples",
|
|
79
|
-
"test": "test"
|
|
164
|
+
"test": "test",
|
|
165
|
+
"dist": "dist",
|
|
166
|
+
"src": "src",
|
|
167
|
+
"types": "types"
|
|
80
168
|
},
|
|
81
169
|
"files": [
|
|
82
170
|
"dist",
|
|
83
171
|
"types",
|
|
84
172
|
"src"
|
|
85
173
|
],
|
|
86
|
-
"sideEffects": false
|
|
174
|
+
"sideEffects": false,
|
|
175
|
+
"funding": [
|
|
176
|
+
{
|
|
177
|
+
"type": "github",
|
|
178
|
+
"url": "https://github.com/sponsors/TarekRaafat"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"type": "buymeacoffee",
|
|
182
|
+
"url": "https://www.buymeacoffee.com/tarekraafat"
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"browserslist": [
|
|
186
|
+
"> 0.25%",
|
|
187
|
+
"last 2 versions",
|
|
188
|
+
"not dead",
|
|
189
|
+
"not op_mini all",
|
|
190
|
+
"not ie 11"
|
|
191
|
+
]
|
|
87
192
|
}
|
package/src/core/Eleva.js
CHANGED
|
@@ -151,6 +151,7 @@ export class Eleva {
|
|
|
151
151
|
const mergedContext = { ...context, ...data };
|
|
152
152
|
const watcherUnsubscribers = [];
|
|
153
153
|
const childInstances = [];
|
|
154
|
+
const cleanupListeners = [];
|
|
154
155
|
|
|
155
156
|
if (!this._isMounted) {
|
|
156
157
|
mergedContext.onBeforeMount && mergedContext.onBeforeMount();
|
|
@@ -168,7 +169,7 @@ export class Eleva {
|
|
|
168
169
|
mergedContext
|
|
169
170
|
);
|
|
170
171
|
this.renderer.patchDOM(container, newHtml);
|
|
171
|
-
this._processEvents(container, mergedContext);
|
|
172
|
+
this._processEvents(container, mergedContext, cleanupListeners);
|
|
172
173
|
this._injectStyles(container, compName, style, mergedContext);
|
|
173
174
|
this._mountChildren(container, children, childInstances);
|
|
174
175
|
if (!this._isMounted) {
|
|
@@ -194,12 +195,13 @@ export class Eleva {
|
|
|
194
195
|
container,
|
|
195
196
|
data: mergedContext,
|
|
196
197
|
/**
|
|
197
|
-
* Unmounts the component, cleaning up watchers, child components, and clearing the container.
|
|
198
|
+
* Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
|
|
198
199
|
*
|
|
199
200
|
* @returns {void}
|
|
200
201
|
*/
|
|
201
202
|
unmount: () => {
|
|
202
203
|
watcherUnsubscribers.forEach((fn) => fn());
|
|
204
|
+
cleanupListeners.forEach((fn) => fn());
|
|
203
205
|
childInstances.forEach((child) => child.unmount());
|
|
204
206
|
mergedContext.onUnmount && mergedContext.onUnmount();
|
|
205
207
|
container.innerHTML = "";
|
|
@@ -228,12 +230,14 @@ export class Eleva {
|
|
|
228
230
|
|
|
229
231
|
/**
|
|
230
232
|
* Processes DOM elements for event binding based on attributes starting with "@".
|
|
233
|
+
* Tracks listeners for cleanup during unmount.
|
|
231
234
|
*
|
|
232
235
|
* @param {HTMLElement} container - The container element in which to search for events.
|
|
233
236
|
* @param {Object<string, any>} context - The current context containing event handler definitions.
|
|
237
|
+
* @param {Array<Function>} cleanupListeners - Array to collect cleanup functions for each event listener.
|
|
234
238
|
* @private
|
|
235
239
|
*/
|
|
236
|
-
_processEvents(container, context) {
|
|
240
|
+
_processEvents(container, context, cleanupListeners) {
|
|
237
241
|
container.querySelectorAll("*").forEach((el) => {
|
|
238
242
|
[...el.attributes].forEach(({ name, value }) => {
|
|
239
243
|
if (name.startsWith("@")) {
|
|
@@ -242,6 +246,7 @@ export class Eleva {
|
|
|
242
246
|
if (typeof handler === "function") {
|
|
243
247
|
el.addEventListener(event, handler);
|
|
244
248
|
el.removeAttribute(name);
|
|
249
|
+
cleanupListeners.push(() => el.removeEventListener(event, handler));
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
});
|
|
@@ -288,7 +293,7 @@ export class Eleva {
|
|
|
288
293
|
const props = {};
|
|
289
294
|
[...childEl.attributes].forEach(({ name, value }) => {
|
|
290
295
|
if (name.startsWith("eleva-prop-")) {
|
|
291
|
-
props[name.
|
|
296
|
+
props[name.replace("eleva-prop-", "")] = value;
|
|
292
297
|
}
|
|
293
298
|
});
|
|
294
299
|
const instance = this.mount(childEl, children[childSelector], props);
|
package/src/modules/Renderer.js
CHANGED
|
@@ -12,11 +12,25 @@ export class Renderer {
|
|
|
12
12
|
*
|
|
13
13
|
* @param {HTMLElement} container - The container element to patch.
|
|
14
14
|
* @param {string} newHtml - The new HTML content to apply.
|
|
15
|
+
* @throws {Error} If container is not an HTMLElement or newHtml is not a string
|
|
15
16
|
*/
|
|
16
17
|
patchDOM(container, newHtml) {
|
|
18
|
+
if (!(container instanceof HTMLElement)) {
|
|
19
|
+
throw new Error("Container must be an HTMLElement");
|
|
20
|
+
}
|
|
21
|
+
if (typeof newHtml !== "string") {
|
|
22
|
+
throw new Error("newHtml must be a string");
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
const tempContainer = document.createElement("div");
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
try {
|
|
27
|
+
tempContainer.innerHTML = newHtml;
|
|
28
|
+
this.diff(container, tempContainer);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to patch DOM: ${error.message}`);
|
|
31
|
+
} finally {
|
|
32
|
+
tempContainer.innerHTML = "";
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -24,36 +38,53 @@ export class Renderer {
|
|
|
24
38
|
*
|
|
25
39
|
* @param {HTMLElement} oldParent - The original DOM element.
|
|
26
40
|
* @param {HTMLElement} newParent - The new DOM element.
|
|
41
|
+
* @throws {Error} If either parent is not an HTMLElement
|
|
27
42
|
*/
|
|
28
43
|
diff(oldParent, newParent) {
|
|
44
|
+
if (
|
|
45
|
+
!(oldParent instanceof HTMLElement) ||
|
|
46
|
+
!(newParent instanceof HTMLElement)
|
|
47
|
+
) {
|
|
48
|
+
throw new Error("Both parents must be HTMLElements");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fast path for identical nodes
|
|
52
|
+
if (oldParent.isEqualNode(newParent)) return;
|
|
53
|
+
|
|
29
54
|
const oldNodes = Array.from(oldParent.childNodes);
|
|
30
55
|
const newNodes = Array.from(newParent.childNodes);
|
|
31
56
|
const max = Math.max(oldNodes.length, newNodes.length);
|
|
57
|
+
|
|
58
|
+
// Batch DOM operations for better performance
|
|
59
|
+
const operations = [];
|
|
60
|
+
|
|
32
61
|
for (let i = 0; i < max; i++) {
|
|
33
62
|
const oldNode = oldNodes[i];
|
|
34
63
|
const newNode = newNodes[i];
|
|
35
64
|
|
|
36
65
|
// Case 1: Append new nodes that don't exist in the old tree.
|
|
37
66
|
if (!oldNode && newNode) {
|
|
38
|
-
oldParent.appendChild(newNode.cloneNode(true));
|
|
67
|
+
operations.push(() => oldParent.appendChild(newNode.cloneNode(true)));
|
|
39
68
|
continue;
|
|
40
69
|
}
|
|
41
70
|
// Case 2: Remove old nodes not present in the new tree.
|
|
42
71
|
if (oldNode && !newNode) {
|
|
43
|
-
oldParent.removeChild(oldNode);
|
|
72
|
+
operations.push(() => oldParent.removeChild(oldNode));
|
|
44
73
|
continue;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
// Case 3: For element nodes, compare keys if available.
|
|
48
77
|
if (
|
|
49
|
-
oldNode
|
|
50
|
-
newNode
|
|
78
|
+
oldNode?.nodeType === Node.ELEMENT_NODE &&
|
|
79
|
+
newNode?.nodeType === Node.ELEMENT_NODE
|
|
51
80
|
) {
|
|
52
81
|
const oldKey = oldNode.getAttribute("key");
|
|
53
82
|
const newKey = newNode.getAttribute("key");
|
|
54
83
|
if (oldKey || newKey) {
|
|
55
84
|
if (oldKey !== newKey) {
|
|
56
|
-
|
|
85
|
+
operations.push(() =>
|
|
86
|
+
oldParent.replaceChild(newNode.cloneNode(true), oldNode)
|
|
87
|
+
);
|
|
57
88
|
continue;
|
|
58
89
|
}
|
|
59
90
|
}
|
|
@@ -61,25 +92,32 @@ export class Renderer {
|
|
|
61
92
|
|
|
62
93
|
// Case 4: Replace nodes if types or tag names differ.
|
|
63
94
|
if (
|
|
64
|
-
oldNode
|
|
65
|
-
oldNode
|
|
95
|
+
oldNode?.nodeType !== newNode?.nodeType ||
|
|
96
|
+
oldNode?.nodeName !== newNode?.nodeName
|
|
66
97
|
) {
|
|
67
|
-
|
|
98
|
+
operations.push(() =>
|
|
99
|
+
oldParent.replaceChild(newNode.cloneNode(true), oldNode)
|
|
100
|
+
);
|
|
68
101
|
continue;
|
|
69
102
|
}
|
|
103
|
+
|
|
70
104
|
// Case 5: For text nodes, update content if different.
|
|
71
|
-
if (oldNode
|
|
105
|
+
if (oldNode?.nodeType === Node.TEXT_NODE) {
|
|
72
106
|
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
73
107
|
oldNode.nodeValue = newNode.nodeValue;
|
|
74
108
|
}
|
|
75
109
|
continue;
|
|
76
110
|
}
|
|
111
|
+
|
|
77
112
|
// Case 6: For element nodes, update attributes and then diff children.
|
|
78
|
-
if (oldNode
|
|
113
|
+
if (oldNode?.nodeType === Node.ELEMENT_NODE) {
|
|
79
114
|
this.updateAttributes(oldNode, newNode);
|
|
80
115
|
this.diff(oldNode, newNode);
|
|
81
116
|
}
|
|
82
117
|
}
|
|
118
|
+
|
|
119
|
+
// Execute batched operations
|
|
120
|
+
operations.forEach((op) => op());
|
|
83
121
|
}
|
|
84
122
|
|
|
85
123
|
/**
|
|
@@ -87,33 +125,74 @@ export class Renderer {
|
|
|
87
125
|
*
|
|
88
126
|
* @param {HTMLElement} oldEl - The element to update.
|
|
89
127
|
* @param {HTMLElement} newEl - The element providing the updated attributes.
|
|
128
|
+
* @throws {Error} If either element is not an HTMLElement
|
|
90
129
|
*/
|
|
91
130
|
updateAttributes(oldEl, newEl) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
if (!(oldEl instanceof HTMLElement) || !(newEl instanceof HTMLElement)) {
|
|
132
|
+
throw new Error("Both elements must be HTMLElements");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Special cases for properties that don't map directly to attributes
|
|
136
|
+
const specialProperties = {
|
|
137
|
+
value: true,
|
|
138
|
+
checked: true,
|
|
139
|
+
selected: true,
|
|
140
|
+
disabled: true,
|
|
141
|
+
readOnly: true,
|
|
142
|
+
multiple: true,
|
|
97
143
|
};
|
|
98
144
|
|
|
99
|
-
//
|
|
145
|
+
// Batch attribute operations for better performance
|
|
146
|
+
const operations = [];
|
|
147
|
+
|
|
148
|
+
// Remove old attributes that no longer exist
|
|
100
149
|
Array.from(oldEl.attributes).forEach((attr) => {
|
|
101
150
|
if (attr.name.startsWith("@")) return;
|
|
102
151
|
if (!newEl.hasAttribute(attr.name)) {
|
|
103
|
-
oldEl.removeAttribute(attr.name);
|
|
152
|
+
operations.push(() => oldEl.removeAttribute(attr.name));
|
|
104
153
|
}
|
|
105
154
|
});
|
|
106
|
-
|
|
155
|
+
|
|
156
|
+
// Add or update attributes from newEl
|
|
107
157
|
Array.from(newEl.attributes).forEach((attr) => {
|
|
108
158
|
if (attr.name.startsWith("@")) return;
|
|
109
159
|
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
160
|
+
operations.push(() => {
|
|
161
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
162
|
+
|
|
163
|
+
// Convert kebab-case to camelCase for property names
|
|
164
|
+
const propName = attr.name.replace(/-([a-z])/g, (_, letter) =>
|
|
165
|
+
letter.toUpperCase()
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Handle special cases first
|
|
169
|
+
if (specialProperties[propName]) {
|
|
170
|
+
oldEl[propName] = attr.value === "" ? true : attr.value;
|
|
171
|
+
}
|
|
172
|
+
// Handle ARIA attributes
|
|
173
|
+
else if (attr.name.startsWith("aria-")) {
|
|
174
|
+
const ariaName =
|
|
175
|
+
"aria" +
|
|
176
|
+
attr.name
|
|
177
|
+
.slice(5)
|
|
178
|
+
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
179
|
+
oldEl[ariaName] = attr.value;
|
|
180
|
+
}
|
|
181
|
+
// Handle data attributes
|
|
182
|
+
else if (attr.name.startsWith("data-")) {
|
|
183
|
+
// dataset handles the camelCase conversion automatically
|
|
184
|
+
const dataName = attr.name.slice(5);
|
|
185
|
+
oldEl.dataset[dataName] = attr.value;
|
|
186
|
+
}
|
|
187
|
+
// Handle standard properties
|
|
188
|
+
else if (propName in oldEl) {
|
|
189
|
+
oldEl[propName] = attr.value;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
116
192
|
}
|
|
117
193
|
});
|
|
194
|
+
|
|
195
|
+
// Execute batched operations
|
|
196
|
+
operations.forEach((op) => op());
|
|
118
197
|
}
|
|
119
198
|
}
|
package/src/modules/Signal.js
CHANGED
|
@@ -17,6 +17,8 @@ export class Signal {
|
|
|
17
17
|
this._value = value;
|
|
18
18
|
/** @private {Set<function>} Collection of callback functions to be notified when value changes */
|
|
19
19
|
this._watchers = new Set();
|
|
20
|
+
/** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
|
|
21
|
+
this._pending = false;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -36,7 +38,7 @@ export class Signal {
|
|
|
36
38
|
set value(newVal) {
|
|
37
39
|
if (newVal !== this._value) {
|
|
38
40
|
this._value = newVal;
|
|
39
|
-
this.
|
|
41
|
+
this._notifyWatchers();
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -50,4 +52,22 @@ export class Signal {
|
|
|
50
52
|
this._watchers.add(fn);
|
|
51
53
|
return () => this._watchers.delete(fn);
|
|
52
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Notifies all registered watchers of a value change using microtask scheduling.
|
|
58
|
+
* Uses a pending flag to batch multiple synchronous updates into a single notification.
|
|
59
|
+
* All watcher callbacks receive the current value when executed.
|
|
60
|
+
*
|
|
61
|
+
* @private
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
_notifyWatchers() {
|
|
65
|
+
if (!this._pending) {
|
|
66
|
+
this._pending = true;
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
this._pending = false;
|
|
69
|
+
this._watchers.forEach((fn) => fn(this._value));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
53
73
|
}
|
|
@@ -15,9 +15,10 @@ export class TemplateEngine {
|
|
|
15
15
|
* @returns {string} The resulting string with evaluated values.
|
|
16
16
|
*/
|
|
17
17
|
static parse(template, data) {
|
|
18
|
+
if (!template || typeof template !== "string") return template;
|
|
19
|
+
|
|
18
20
|
return template.replace(/\{\{\s*(.*?)\s*\}\}/g, (_, expr) => {
|
|
19
|
-
|
|
20
|
-
return value === undefined ? "" : value;
|
|
21
|
+
return this.evaluate(expr, data);
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,11 +30,11 @@ export class TemplateEngine {
|
|
|
29
30
|
* @returns {any} The result of the evaluated expression, or an empty string if undefined or on error.
|
|
30
31
|
*/
|
|
31
32
|
static evaluate(expr, data) {
|
|
33
|
+
if (!expr || typeof expr !== "string") return expr;
|
|
34
|
+
|
|
32
35
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const result = new Function(...keys, `return ${expr}`)(...values);
|
|
36
|
-
return result === undefined ? "" : result;
|
|
36
|
+
const compiledFn = new Function("data", `with(data) { return ${expr} }`);
|
|
37
|
+
return compiledFn(data);
|
|
37
38
|
} catch (error) {
|
|
38
39
|
console.error(`Template evaluation error:`, {
|
|
39
40
|
expression: expr,
|
package/types/core/Eleva.d.ts
CHANGED
|
@@ -95,9 +95,11 @@ export 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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Eleva.d.ts","sourceRoot":"","sources":["../../src/core/Eleva.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;GAIG;AACH;IACE;;;;;OAKG;IACH,kBAHW,MAAM;;OA0BhB;IAtBC,wEAAwE;IACxE,MADW,MAAM,CACD;IAChB,uFAAuF;IACvF;;MAAoB;IACpB,0GAA0G;IAC1G;;MAAqB;IACrB,wEAAwE;IACxE,iBAAkB;IAClB,mFAAmF;IACnF,wBAMC;IACD,2EAA2E;IAC3E,mBAAuB;IACvB,qFAAqF;IACrF,gBAA4B;IAC5B,yFAAyF;IACzF,iBAA8B;IAGhC;;;;;;OAMG;IACH,YAJW,MAAM;;QAEJ,KAAK,CAQjB;IAED;;;;;;OAMG;IACH,gBAJW,MAAM,cACN,mBAAmB,GACjB,KAAK,CAKjB;IAED;;;;;;;;OAQG;IACH,iBANW,WAAW,YACX,MAAM,GAAC,mBAAmB;;QAExB,MAAM,GAAC,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"Eleva.d.ts","sourceRoot":"","sources":["../../src/core/Eleva.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;GAIG;AACH;IACE;;;;;OAKG;IACH,kBAHW,MAAM;;OA0BhB;IAtBC,wEAAwE;IACxE,MADW,MAAM,CACD;IAChB,uFAAuF;IACvF;;MAAoB;IACpB,0GAA0G;IAC1G;;MAAqB;IACrB,wEAAwE;IACxE,iBAAkB;IAClB,mFAAmF;IACnF,wBAMC;IACD,2EAA2E;IAC3E,mBAAuB;IACvB,qFAAqF;IACrF,gBAA4B;IAC5B,yFAAyF;IACzF,iBAA8B;IAGhC;;;;;;OAMG;IACH,YAJW,MAAM;;QAEJ,KAAK,CAQjB;IAED;;;;;;OAMG;IACH,gBAJW,MAAM,cACN,mBAAmB,GACjB,KAAK,CAKjB;IAED;;;;;;;;OAQG;IACH,iBANW,WAAW,YACX,MAAM,GAAC,mBAAmB;;QAExB,MAAM,GAAC,OAAO,CAAC,MAAM,CAAC,CAkHlC;IAED;;;;;OAKG;IACH,+BAKC;IAED;;;;;;;;OAQG;IACH,uBAcC;IAED;;;;;;;;OAQG;IACH,sBAYC;IAED;;;;;;;OAOG;IACH,uBAgBC;CACF;;;;;;;;;;;;UArS4C,CAAC;YAAO,MAAM,GAAE,GAAG;KAAC,GAAC,OAAO,CAAC;YAAO,MAAM,GAAE,GAAG;KAAC,CAAC,CAAC;;;;;;cAKjF,CAAS,IAAmB,EAAnB;YAAO,MAAM,GAAE,GAAG;KAAC,KAAG,MAAM;;;;;;;;UAKN,MAAM"}
|
|
@@ -10,6 +10,7 @@ export class Renderer {
|
|
|
10
10
|
*
|
|
11
11
|
* @param {HTMLElement} container - The container element to patch.
|
|
12
12
|
* @param {string} newHtml - The new HTML content to apply.
|
|
13
|
+
* @throws {Error} If container is not an HTMLElement or newHtml is not a string
|
|
13
14
|
*/
|
|
14
15
|
patchDOM(container: HTMLElement, newHtml: string): void;
|
|
15
16
|
/**
|
|
@@ -17,6 +18,7 @@ export class Renderer {
|
|
|
17
18
|
*
|
|
18
19
|
* @param {HTMLElement} oldParent - The original DOM element.
|
|
19
20
|
* @param {HTMLElement} newParent - The new DOM element.
|
|
21
|
+
* @throws {Error} If either parent is not an HTMLElement
|
|
20
22
|
*/
|
|
21
23
|
diff(oldParent: HTMLElement, newParent: HTMLElement): void;
|
|
22
24
|
/**
|
|
@@ -24,6 +26,7 @@ export class Renderer {
|
|
|
24
26
|
*
|
|
25
27
|
* @param {HTMLElement} oldEl - The element to update.
|
|
26
28
|
* @param {HTMLElement} newEl - The element providing the updated attributes.
|
|
29
|
+
* @throws {Error} If either element is not an HTMLElement
|
|
27
30
|
*/
|
|
28
31
|
updateAttributes(oldEl: HTMLElement, newEl: HTMLElement): void;
|
|
29
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Renderer.d.ts","sourceRoot":"","sources":["../../src/modules/Renderer.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE
|
|
1
|
+
{"version":3,"file":"Renderer.d.ts","sourceRoot":"","sources":["../../src/modules/Renderer.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH,oBAJW,WAAW,WACX,MAAM,QAoBhB;IAED;;;;;;OAMG;IACH,gBAJW,WAAW,aACX,WAAW,QAiFrB;IAED;;;;;;OAMG;IACH,wBAJW,WAAW,SACX,WAAW,QAsErB;CACF"}
|
|
@@ -15,6 +15,8 @@ export class Signal {
|
|
|
15
15
|
private _value;
|
|
16
16
|
/** @private {Set<function>} Collection of callback functions to be notified when value changes */
|
|
17
17
|
private _watchers;
|
|
18
|
+
/** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
|
|
19
|
+
private _pending;
|
|
18
20
|
/**
|
|
19
21
|
* Sets a new value for the signal and notifies all registered watchers if the value has changed.
|
|
20
22
|
*
|
|
@@ -34,5 +36,14 @@ export class Signal {
|
|
|
34
36
|
* @returns {function(): boolean} A function to unsubscribe the watcher.
|
|
35
37
|
*/
|
|
36
38
|
watch(fn: (arg0: any) => void): () => boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Notifies all registered watchers of a value change using microtask scheduling.
|
|
41
|
+
* Uses a pending flag to batch multiple synchronous updates into a single notification.
|
|
42
|
+
* All watcher callbacks receive the current value when executed.
|
|
43
|
+
*
|
|
44
|
+
* @private
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
private _notifyWatchers;
|
|
37
48
|
}
|
|
38
49
|
//# sourceMappingURL=Signal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../../src/modules/Signal.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;OAIG;IACH,mBAFW,GAAC,
|
|
1
|
+
{"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../../src/modules/Signal.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;OAIG;IACH,mBAFW,GAAC,EASX;IANC,mEAAmE;IACnE,eAAmB;IACnB,kGAAkG;IAClG,kBAA0B;IAC1B,sHAAsH;IACtH,iBAAqB;IAYvB;;;;OAIG;IACH,kBAFW,GAAC,EAOX;IAnBD;;;;OAIG;IACH,aAFa,GAAC,CAIb;IAcD;;;;;OAKG;IACH,UAHW,CAAS,IAAG,EAAH,GAAG,KAAG,IAAI,GACjB,MAAY,OAAO,CAK/B;IAED;;;;;;;OAOG;IACH,wBAQC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplateEngine.d.ts","sourceRoot":"","sources":["../../src/modules/TemplateEngine.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH,uBAJW,MAAM;;QAEJ,MAAM,
|
|
1
|
+
{"version":3,"file":"TemplateEngine.d.ts","sourceRoot":"","sources":["../../src/modules/TemplateEngine.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH,uBAJW,MAAM;;QAEJ,MAAM,CAQlB;IAED;;;;;;OAMG;IACH,sBAJW,MAAM;;QAEJ,GAAG,CAgBf;CACF"}
|