eleva 1.0.0-rc.1 → 1.0.0-rc.11
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 +505 -41
- package/dist/eleva-plugins.cjs.js +3397 -0
- package/dist/eleva-plugins.cjs.js.map +1 -0
- package/dist/eleva-plugins.esm.js +3392 -0
- package/dist/eleva-plugins.esm.js.map +1 -0
- package/dist/eleva-plugins.umd.js +3403 -0
- package/dist/eleva-plugins.umd.js.map +1 -0
- package/dist/eleva-plugins.umd.min.js +3 -0
- package/dist/eleva-plugins.umd.min.js.map +1 -0
- package/dist/eleva.cjs.js +617 -118
- package/dist/eleva.cjs.js.map +1 -1
- package/dist/eleva.d.ts +612 -75
- package/dist/eleva.esm.js +617 -118
- package/dist/eleva.esm.js.map +1 -1
- package/dist/eleva.umd.js +617 -118
- package/dist/eleva.umd.js.map +1 -1
- package/dist/eleva.umd.min.js +2 -2
- package/dist/eleva.umd.min.js.map +1 -1
- package/dist/plugins/attr.umd.js +232 -0
- package/dist/plugins/attr.umd.js.map +1 -0
- package/dist/plugins/attr.umd.min.js +3 -0
- package/dist/plugins/attr.umd.min.js.map +1 -0
- package/dist/plugins/props.umd.js +712 -0
- package/dist/plugins/props.umd.js.map +1 -0
- package/dist/plugins/props.umd.min.js +3 -0
- package/dist/plugins/props.umd.min.js.map +1 -0
- package/dist/plugins/router.umd.js +1808 -0
- package/dist/plugins/router.umd.js.map +1 -0
- package/dist/plugins/router.umd.min.js +3 -0
- package/dist/plugins/router.umd.min.js.map +1 -0
- package/dist/plugins/store.umd.js +685 -0
- package/dist/plugins/store.umd.js.map +1 -0
- package/dist/plugins/store.umd.min.js +3 -0
- package/dist/plugins/store.umd.min.js.map +1 -0
- package/package.json +107 -45
- package/src/core/Eleva.js +247 -63
- package/src/modules/Emitter.js +98 -8
- package/src/modules/Renderer.js +66 -36
- package/src/modules/Signal.js +85 -8
- package/src/modules/TemplateEngine.js +121 -13
- package/src/plugins/Attr.js +255 -0
- package/src/plugins/Props.js +593 -0
- package/src/plugins/Router.js +1922 -0
- package/src/plugins/Store.js +744 -0
- package/src/plugins/index.js +40 -0
- package/types/core/Eleva.d.ts +217 -50
- package/types/core/Eleva.d.ts.map +1 -1
- package/types/modules/Emitter.d.ts +111 -12
- package/types/modules/Emitter.d.ts.map +1 -1
- package/types/modules/Renderer.d.ts +68 -3
- package/types/modules/Renderer.d.ts.map +1 -1
- package/types/modules/Signal.d.ts +92 -10
- package/types/modules/Signal.d.ts.map +1 -1
- package/types/modules/TemplateEngine.d.ts +131 -15
- package/types/modules/TemplateEngine.d.ts.map +1 -1
- package/types/plugins/Attr.d.ts +29 -0
- package/types/plugins/Attr.d.ts.map +1 -0
- package/types/plugins/Props.d.ts +49 -0
- package/types/plugins/Props.d.ts.map +1 -0
- package/types/plugins/Router.d.ts +1000 -0
- package/types/plugins/Router.d.ts.map +1 -0
- package/types/plugins/Store.d.ts +87 -0
- package/types/plugins/Store.d.ts.map +1 -0
- package/types/plugins/index.d.ts +5 -0
- package/types/plugins/index.d.ts.map +1 -0
package/dist/eleva.esm.js
CHANGED
|
@@ -1,18 +1,76 @@
|
|
|
1
|
-
/*! Eleva v1.0.0-rc.
|
|
1
|
+
/*! Eleva v1.0.0-rc.11 | MIT License | https://elevajs.com */
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Record<string, unknown>} TemplateData
|
|
8
|
+
* Data context for template interpolation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {string} TemplateString
|
|
13
|
+
* A string containing {{ expression }} interpolation markers
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {string} Expression
|
|
18
|
+
* A JavaScript expression to be evaluated in the data context
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {unknown} EvaluationResult
|
|
23
|
+
* The result of evaluating an expression (string, number, boolean, object, etc.)
|
|
24
|
+
*/
|
|
25
|
+
|
|
2
26
|
/**
|
|
3
27
|
* @class 🔒 TemplateEngine
|
|
4
28
|
* @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
|
|
5
|
-
* Provides a
|
|
29
|
+
* Provides a way to evaluate expressions in templates.
|
|
6
30
|
* All methods are static and can be called directly on the class.
|
|
7
31
|
*
|
|
32
|
+
* Template Syntax:
|
|
33
|
+
* - `{{ expression }}` - Interpolate any JavaScript expression
|
|
34
|
+
* - `{{ variable }}` - Access data properties directly
|
|
35
|
+
* - `{{ object.property }}` - Access nested properties
|
|
36
|
+
* - `{{ condition ? a : b }}` - Ternary expressions
|
|
37
|
+
* - `{{ func(arg) }}` - Call functions from data context
|
|
38
|
+
*
|
|
8
39
|
* @example
|
|
40
|
+
* // Basic interpolation
|
|
9
41
|
* const template = "Hello, {{name}}!";
|
|
10
42
|
* const data = { name: "World" };
|
|
11
|
-
* const result = TemplateEngine.parse(template, data);
|
|
43
|
+
* const result = TemplateEngine.parse(template, data);
|
|
44
|
+
* // Result: "Hello, World!"
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Nested properties
|
|
48
|
+
* const template = "Welcome, {{user.name}}!";
|
|
49
|
+
* const data = { user: { name: "John" } };
|
|
50
|
+
* const result = TemplateEngine.parse(template, data);
|
|
51
|
+
* // Result: "Welcome, John!"
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Expressions
|
|
55
|
+
* const template = "Status: {{active ? 'Online' : 'Offline'}}";
|
|
56
|
+
* const data = { active: true };
|
|
57
|
+
* const result = TemplateEngine.parse(template, data);
|
|
58
|
+
* // Result: "Status: Online"
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // With Signal values
|
|
62
|
+
* const template = "Count: {{count.value}}";
|
|
63
|
+
* const data = { count: { value: 42 } };
|
|
64
|
+
* const result = TemplateEngine.parse(template, data);
|
|
65
|
+
* // Result: "Count: 42"
|
|
12
66
|
*/
|
|
13
67
|
class TemplateEngine {
|
|
14
68
|
/**
|
|
15
|
-
*
|
|
69
|
+
* Regular expression for matching template expressions in the format {{ expression }}
|
|
70
|
+
* Matches: {{ anything }} with optional whitespace inside braces
|
|
71
|
+
*
|
|
72
|
+
* @static
|
|
73
|
+
* @private
|
|
16
74
|
* @type {RegExp}
|
|
17
75
|
*/
|
|
18
76
|
static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
|
|
@@ -23,13 +81,37 @@ class TemplateEngine {
|
|
|
23
81
|
*
|
|
24
82
|
* @public
|
|
25
83
|
* @static
|
|
26
|
-
* @param {
|
|
27
|
-
* @param {
|
|
84
|
+
* @param {TemplateString|unknown} template - The template string to parse.
|
|
85
|
+
* @param {TemplateData} data - The data context for evaluating expressions.
|
|
28
86
|
* @returns {string} The parsed template with expressions replaced by their values.
|
|
87
|
+
*
|
|
29
88
|
* @example
|
|
30
|
-
*
|
|
89
|
+
* // Simple variables
|
|
90
|
+
* TemplateEngine.parse("Hello, {{name}}!", { name: "World" });
|
|
91
|
+
* // Result: "Hello, World!"
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Nested properties
|
|
95
|
+
* TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
|
|
31
96
|
* user: { name: "John", age: 30 }
|
|
32
|
-
* });
|
|
97
|
+
* });
|
|
98
|
+
* // Result: "John is 30 years old"
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Multiple expressions
|
|
102
|
+
* TemplateEngine.parse("{{greeting}}, {{name}}! You have {{count}} messages.", {
|
|
103
|
+
* greeting: "Hello",
|
|
104
|
+
* name: "User",
|
|
105
|
+
* count: 5
|
|
106
|
+
* });
|
|
107
|
+
* // Result: "Hello, User! You have 5 messages."
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // With conditionals
|
|
111
|
+
* TemplateEngine.parse("Status: {{online ? 'Active' : 'Inactive'}}", {
|
|
112
|
+
* online: true
|
|
113
|
+
* });
|
|
114
|
+
* // Result: "Status: Active"
|
|
33
115
|
*/
|
|
34
116
|
static parse(template, data) {
|
|
35
117
|
if (typeof template !== "string") return template;
|
|
@@ -38,18 +120,44 @@ class TemplateEngine {
|
|
|
38
120
|
|
|
39
121
|
/**
|
|
40
122
|
* Evaluates an expression in the context of the provided data object.
|
|
123
|
+
*
|
|
41
124
|
* Note: This does not provide a true sandbox and evaluated expressions may access global scope.
|
|
42
125
|
* The use of the `with` statement is necessary for expression evaluation but has security implications.
|
|
43
|
-
*
|
|
126
|
+
* Only use with trusted templates. User input should never be directly interpolated.
|
|
44
127
|
*
|
|
45
128
|
* @public
|
|
46
129
|
* @static
|
|
47
|
-
* @param {
|
|
48
|
-
* @param {
|
|
49
|
-
* @returns {
|
|
130
|
+
* @param {Expression|unknown} expression - The expression to evaluate.
|
|
131
|
+
* @param {TemplateData} data - The data context for evaluation.
|
|
132
|
+
* @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // Property access
|
|
136
|
+
* TemplateEngine.evaluate("user.name", { user: { name: "John" } });
|
|
137
|
+
* // Result: "John"
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* // Numeric values
|
|
141
|
+
* TemplateEngine.evaluate("user.age", { user: { age: 30 } });
|
|
142
|
+
* // Result: 30
|
|
143
|
+
*
|
|
50
144
|
* @example
|
|
51
|
-
*
|
|
52
|
-
*
|
|
145
|
+
* // Expressions
|
|
146
|
+
* TemplateEngine.evaluate("items.length > 0", { items: [1, 2, 3] });
|
|
147
|
+
* // Result: true
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Function calls
|
|
151
|
+
* TemplateEngine.evaluate("formatDate(date)", {
|
|
152
|
+
* date: new Date(),
|
|
153
|
+
* formatDate: (d) => d.toISOString()
|
|
154
|
+
* });
|
|
155
|
+
* // Result: "2024-01-01T00:00:00.000Z"
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* // Failed evaluation returns empty string
|
|
159
|
+
* TemplateEngine.evaluate("nonexistent.property", {});
|
|
160
|
+
* // Result: ""
|
|
53
161
|
*/
|
|
54
162
|
static evaluate(expression, data) {
|
|
55
163
|
if (typeof expression !== "string") return expression;
|
|
@@ -61,6 +169,29 @@ class TemplateEngine {
|
|
|
61
169
|
}
|
|
62
170
|
}
|
|
63
171
|
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @template T
|
|
178
|
+
* @callback SignalWatcher
|
|
179
|
+
* @param {T} value - The new value of the signal
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @callback SignalUnsubscribe
|
|
185
|
+
* @returns {boolean} True if the watcher was successfully removed
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @template T
|
|
190
|
+
* @typedef {Object} SignalLike
|
|
191
|
+
* @property {T} value - The current value
|
|
192
|
+
* @property {function(SignalWatcher<T>): SignalUnsubscribe} watch - Subscribe to changes
|
|
193
|
+
*/
|
|
194
|
+
|
|
64
195
|
/**
|
|
65
196
|
* @class ⚡ Signal
|
|
66
197
|
* @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
|
|
@@ -69,11 +200,29 @@ class TemplateEngine {
|
|
|
69
200
|
* Updates are batched using microtasks to prevent multiple synchronous notifications.
|
|
70
201
|
* The class is generic, allowing type-safe handling of any value type T.
|
|
71
202
|
*
|
|
203
|
+
* @template T The type of value held by this signal
|
|
204
|
+
*
|
|
72
205
|
* @example
|
|
206
|
+
* // Basic usage
|
|
73
207
|
* const count = new Signal(0);
|
|
74
208
|
* count.watch((value) => console.log(`Count changed to: ${value}`));
|
|
75
209
|
* count.value = 1; // Logs: "Count changed to: 1"
|
|
76
|
-
*
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* // With unsubscribe
|
|
213
|
+
* const name = new Signal("John");
|
|
214
|
+
* const unsubscribe = name.watch((value) => console.log(value));
|
|
215
|
+
* name.value = "Jane"; // Logs: "Jane"
|
|
216
|
+
* unsubscribe(); // Stop watching
|
|
217
|
+
* name.value = "Bob"; // No log output
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* // With objects
|
|
221
|
+
* /** @type {Signal<{x: number, y: number}>} *\/
|
|
222
|
+
* const position = new Signal({ x: 0, y: 0 });
|
|
223
|
+
* position.value = { x: 10, y: 20 }; // Triggers watchers
|
|
224
|
+
*
|
|
225
|
+
* @implements {SignalLike<T>}
|
|
77
226
|
*/
|
|
78
227
|
class Signal {
|
|
79
228
|
/**
|
|
@@ -81,13 +230,39 @@ class Signal {
|
|
|
81
230
|
*
|
|
82
231
|
* @public
|
|
83
232
|
* @param {T} value - The initial value of the signal.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // Primitive types
|
|
236
|
+
* const count = new Signal(0); // Signal<number>
|
|
237
|
+
* const name = new Signal("John"); // Signal<string>
|
|
238
|
+
* const active = new Signal(true); // Signal<boolean>
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* // Complex types (use JSDoc for type inference)
|
|
242
|
+
* /** @type {Signal<string[]>} *\/
|
|
243
|
+
* const items = new Signal([]);
|
|
244
|
+
*
|
|
245
|
+
* /** @type {Signal<{id: number, name: string} | null>} *\/
|
|
246
|
+
* const user = new Signal(null);
|
|
84
247
|
*/
|
|
85
248
|
constructor(value) {
|
|
86
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Internal storage for the signal's current value
|
|
251
|
+
* @private
|
|
252
|
+
* @type {T}
|
|
253
|
+
*/
|
|
87
254
|
this._value = value;
|
|
88
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Collection of callback functions to be notified when value changes
|
|
257
|
+
* @private
|
|
258
|
+
* @type {Set<SignalWatcher<T>>}
|
|
259
|
+
*/
|
|
89
260
|
this._watchers = new Set();
|
|
90
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Flag to prevent multiple synchronous watcher notifications
|
|
263
|
+
* @private
|
|
264
|
+
* @type {boolean}
|
|
265
|
+
*/
|
|
91
266
|
this._pending = false;
|
|
92
267
|
}
|
|
93
268
|
|
|
@@ -120,12 +295,22 @@ class Signal {
|
|
|
120
295
|
* The watcher will receive the new value as its argument.
|
|
121
296
|
*
|
|
122
297
|
* @public
|
|
123
|
-
* @param {
|
|
124
|
-
* @returns {
|
|
298
|
+
* @param {SignalWatcher<T>} fn - The callback function to invoke on value change.
|
|
299
|
+
* @returns {SignalUnsubscribe} A function to unsubscribe the watcher.
|
|
300
|
+
*
|
|
125
301
|
* @example
|
|
302
|
+
* // Basic watching
|
|
126
303
|
* const unsubscribe = signal.watch((value) => console.log(value));
|
|
127
|
-
*
|
|
128
|
-
*
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* // Stop watching
|
|
307
|
+
* unsubscribe(); // Returns true if watcher was removed
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* // Multiple watchers
|
|
311
|
+
* const unsub1 = signal.watch((v) => console.log("Watcher 1:", v));
|
|
312
|
+
* const unsub2 = signal.watch((v) => console.log("Watcher 2:", v));
|
|
313
|
+
* signal.value = "test"; // Both watchers are called
|
|
129
314
|
*/
|
|
130
315
|
watch(fn) {
|
|
131
316
|
this._watchers.add(fn);
|
|
@@ -151,6 +336,34 @@ class Signal {
|
|
|
151
336
|
}
|
|
152
337
|
}
|
|
153
338
|
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
|
|
341
|
+
// ============================================================================
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @template T
|
|
345
|
+
* @callback EventHandler
|
|
346
|
+
* @param {...T} args - Event arguments
|
|
347
|
+
* @returns {void|Promise<void>}
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @callback EventUnsubscribe
|
|
352
|
+
* @returns {void}
|
|
353
|
+
*/
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @typedef {`${string}:${string}`} EventName
|
|
357
|
+
* Event names follow the format 'namespace:action' (e.g., 'user:login', 'cart:update')
|
|
358
|
+
*/
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @typedef {Object} EmitterLike
|
|
362
|
+
* @property {function(string, EventHandler<unknown>): EventUnsubscribe} on - Subscribe to an event
|
|
363
|
+
* @property {function(string, EventHandler<unknown>=): void} off - Unsubscribe from an event
|
|
364
|
+
* @property {function(string, ...unknown): void} emit - Emit an event
|
|
365
|
+
*/
|
|
366
|
+
|
|
154
367
|
/**
|
|
155
368
|
* @class 📡 Emitter
|
|
156
369
|
* @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
|
|
@@ -158,21 +371,58 @@ class Signal {
|
|
|
158
371
|
* and reactive updates across the application.
|
|
159
372
|
* Events are handled synchronously in the order they were registered, with proper cleanup
|
|
160
373
|
* of unsubscribed handlers.
|
|
161
|
-
*
|
|
374
|
+
*
|
|
375
|
+
* Event names should follow the format 'namespace:action' for consistency and organization.
|
|
162
376
|
*
|
|
163
377
|
* @example
|
|
378
|
+
* // Basic usage
|
|
164
379
|
* const emitter = new Emitter();
|
|
165
380
|
* emitter.on('user:login', (user) => console.log(`User logged in: ${user.name}`));
|
|
166
381
|
* emitter.emit('user:login', { name: 'John' }); // Logs: "User logged in: John"
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* // With unsubscribe
|
|
385
|
+
* const unsub = emitter.on('cart:update', (items) => {
|
|
386
|
+
* console.log(`Cart has ${items.length} items`);
|
|
387
|
+
* });
|
|
388
|
+
* emitter.emit('cart:update', [{ id: 1, name: 'Book' }]); // Logs: "Cart has 1 items"
|
|
389
|
+
* unsub(); // Stop listening
|
|
390
|
+
* emitter.emit('cart:update', []); // No log output
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* // Multiple arguments
|
|
394
|
+
* emitter.on('order:placed', (orderId, amount, currency) => {
|
|
395
|
+
* console.log(`Order ${orderId}: ${amount} ${currency}`);
|
|
396
|
+
* });
|
|
397
|
+
* emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* // Common event patterns
|
|
401
|
+
* // Lifecycle events
|
|
402
|
+
* emitter.on('component:mount', (component) => {});
|
|
403
|
+
* emitter.on('component:unmount', (component) => {});
|
|
404
|
+
* // State events
|
|
405
|
+
* emitter.on('state:change', (newState, oldState) => {});
|
|
406
|
+
* // Navigation events
|
|
407
|
+
* emitter.on('router:navigate', (to, from) => {});
|
|
408
|
+
*
|
|
409
|
+
* @implements {EmitterLike}
|
|
167
410
|
*/
|
|
168
411
|
class Emitter {
|
|
169
412
|
/**
|
|
170
413
|
* Creates a new Emitter instance.
|
|
171
414
|
*
|
|
172
415
|
* @public
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* const emitter = new Emitter();
|
|
173
419
|
*/
|
|
174
420
|
constructor() {
|
|
175
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* Map of event names to their registered handler functions
|
|
423
|
+
* @private
|
|
424
|
+
* @type {Map<string, Set<EventHandler<unknown>>>}
|
|
425
|
+
*/
|
|
176
426
|
this._events = new Map();
|
|
177
427
|
}
|
|
178
428
|
|
|
@@ -182,12 +432,23 @@ class Emitter {
|
|
|
182
432
|
* Event names should follow the format 'namespace:action' for consistency.
|
|
183
433
|
*
|
|
184
434
|
* @public
|
|
435
|
+
* @template T
|
|
185
436
|
* @param {string} event - The name of the event to listen for (e.g., 'user:login').
|
|
186
|
-
* @param {
|
|
187
|
-
* @returns {
|
|
437
|
+
* @param {EventHandler<T>} handler - The callback function to invoke when the event occurs.
|
|
438
|
+
* @returns {EventUnsubscribe} A function to unsubscribe the event handler.
|
|
439
|
+
*
|
|
188
440
|
* @example
|
|
441
|
+
* // Basic subscription
|
|
189
442
|
* const unsubscribe = emitter.on('user:login', (user) => console.log(user));
|
|
190
|
-
*
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* // Typed handler
|
|
446
|
+
* emitter.on('user:update', (/** @type {{id: number, name: string}} *\/ user) => {
|
|
447
|
+
* console.log(`User ${user.id}: ${user.name}`);
|
|
448
|
+
* });
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* // Cleanup
|
|
191
452
|
* unsubscribe(); // Stops listening for the event
|
|
192
453
|
*/
|
|
193
454
|
on(event, handler) {
|
|
@@ -202,12 +463,18 @@ class Emitter {
|
|
|
202
463
|
* Automatically cleans up empty event sets to prevent memory leaks.
|
|
203
464
|
*
|
|
204
465
|
* @public
|
|
466
|
+
* @template T
|
|
205
467
|
* @param {string} event - The name of the event to remove handlers from.
|
|
206
|
-
* @param {
|
|
468
|
+
* @param {EventHandler<T>} [handler] - The specific handler function to remove.
|
|
207
469
|
* @returns {void}
|
|
470
|
+
*
|
|
208
471
|
* @example
|
|
209
472
|
* // Remove a specific handler
|
|
473
|
+
* const loginHandler = (user) => console.log(user);
|
|
474
|
+
* emitter.on('user:login', loginHandler);
|
|
210
475
|
* emitter.off('user:login', loginHandler);
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
211
478
|
* // Remove all handlers for an event
|
|
212
479
|
* emitter.off('user:login');
|
|
213
480
|
*/
|
|
@@ -229,14 +496,22 @@ class Emitter {
|
|
|
229
496
|
* If no handlers are registered for the event, the emission is silently ignored.
|
|
230
497
|
*
|
|
231
498
|
* @public
|
|
499
|
+
* @template T
|
|
232
500
|
* @param {string} event - The name of the event to emit.
|
|
233
|
-
* @param {...
|
|
501
|
+
* @param {...T} args - Optional arguments to pass to the event handlers.
|
|
234
502
|
* @returns {void}
|
|
503
|
+
*
|
|
235
504
|
* @example
|
|
236
505
|
* // Emit an event with data
|
|
237
506
|
* emitter.emit('user:login', { name: 'John', role: 'admin' });
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
238
509
|
* // Emit an event with multiple arguments
|
|
239
|
-
* emitter.emit('
|
|
510
|
+
* emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* // Emit without data
|
|
514
|
+
* emitter.emit('app:ready');
|
|
240
515
|
*/
|
|
241
516
|
emit(event, ...args) {
|
|
242
517
|
if (!this._events.has(event)) return;
|
|
@@ -244,12 +519,27 @@ class Emitter {
|
|
|
244
519
|
}
|
|
245
520
|
}
|
|
246
521
|
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
|
|
524
|
+
// ============================================================================
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @typedef {Object} PatchOptions
|
|
528
|
+
* @property {boolean} [preserveStyles=true]
|
|
529
|
+
* Whether to preserve style elements with data-e-style attribute
|
|
530
|
+
* @property {boolean} [preserveInstances=true]
|
|
531
|
+
* Whether to preserve elements with _eleva_instance property
|
|
532
|
+
*/
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @typedef {Map<string, Node>} KeyMap
|
|
536
|
+
* Map of key attribute values to their corresponding DOM nodes
|
|
537
|
+
*/
|
|
538
|
+
|
|
247
539
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* @type {RegExp}
|
|
540
|
+
* @typedef {'ELEMENT_NODE'|'TEXT_NODE'|'COMMENT_NODE'|'DOCUMENT_FRAGMENT_NODE'} NodeTypeName
|
|
541
|
+
* Common DOM node type names
|
|
251
542
|
*/
|
|
252
|
-
const CAMEL_RE = /-([a-z])/g;
|
|
253
543
|
|
|
254
544
|
/**
|
|
255
545
|
* @class 🎨 Renderer
|
|
@@ -267,27 +557,50 @@ const CAMEL_RE = /-([a-z])/g;
|
|
|
267
557
|
* It's particularly optimized for frequent updates and complex DOM structures.
|
|
268
558
|
*
|
|
269
559
|
* @example
|
|
560
|
+
* // Basic usage
|
|
270
561
|
* const renderer = new Renderer();
|
|
271
562
|
* const container = document.getElementById("app");
|
|
272
563
|
* const newHtml = "<div>Updated content</div>";
|
|
273
564
|
* renderer.patchDOM(container, newHtml);
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* // With keyed elements for optimal list updates
|
|
568
|
+
* const listHtml = `
|
|
569
|
+
* <ul>
|
|
570
|
+
* <li key="item-1">First</li>
|
|
571
|
+
* <li key="item-2">Second</li>
|
|
572
|
+
* <li key="item-3">Third</li>
|
|
573
|
+
* </ul>
|
|
574
|
+
* `;
|
|
575
|
+
* renderer.patchDOM(container, listHtml);
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* // The renderer preserves Eleva-managed elements
|
|
579
|
+
* // Elements with _eleva_instance are not replaced during diffing
|
|
580
|
+
* // Style elements with data-e-style are preserved
|
|
274
581
|
*/
|
|
275
582
|
class Renderer {
|
|
276
583
|
/**
|
|
277
584
|
* Creates a new Renderer instance.
|
|
585
|
+
*
|
|
278
586
|
* @public
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* const renderer = new Renderer();
|
|
279
590
|
*/
|
|
280
591
|
constructor() {
|
|
281
592
|
/**
|
|
282
593
|
* A temporary container to hold the new HTML content while diffing.
|
|
594
|
+
* Reused across patch operations to minimize memory allocation.
|
|
283
595
|
* @private
|
|
284
|
-
* @type {
|
|
596
|
+
* @type {HTMLDivElement}
|
|
285
597
|
*/
|
|
286
598
|
this._tempContainer = document.createElement("div");
|
|
287
599
|
}
|
|
288
600
|
|
|
289
601
|
/**
|
|
290
602
|
* Patches the DOM of the given container with the provided HTML string.
|
|
603
|
+
* Uses an optimized diffing algorithm to minimize DOM operations.
|
|
291
604
|
*
|
|
292
605
|
* @public
|
|
293
606
|
* @param {HTMLElement} container - The container element to patch.
|
|
@@ -295,6 +608,18 @@ class Renderer {
|
|
|
295
608
|
* @returns {void}
|
|
296
609
|
* @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
|
|
297
610
|
* @throws {Error} If DOM patching fails.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* // Update container content
|
|
614
|
+
* renderer.patchDOM(container, '<div class="updated">New content</div>');
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* // Update list with keys for optimal diffing
|
|
618
|
+
* const items = ['a', 'b', 'c'];
|
|
619
|
+
* const html = items.map(item =>
|
|
620
|
+
* `<li key="${item}">${item}</li>`
|
|
621
|
+
* ).join('');
|
|
622
|
+
* renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
|
|
298
623
|
*/
|
|
299
624
|
patchDOM(container, newHtml) {
|
|
300
625
|
if (!(container instanceof HTMLElement)) {
|
|
@@ -412,35 +737,24 @@ class Renderer {
|
|
|
412
737
|
const oldAttrs = oldEl.attributes;
|
|
413
738
|
const newAttrs = newEl.attributes;
|
|
414
739
|
|
|
415
|
-
//
|
|
740
|
+
// Process new attributes
|
|
416
741
|
for (let i = 0; i < newAttrs.length; i++) {
|
|
417
742
|
const {
|
|
418
743
|
name,
|
|
419
744
|
value
|
|
420
745
|
} = newAttrs[i];
|
|
746
|
+
|
|
747
|
+
// Skip event attributes (handled by event system)
|
|
421
748
|
if (name.startsWith("@")) continue;
|
|
749
|
+
|
|
750
|
+
// Skip if attribute hasn't changed
|
|
422
751
|
if (oldEl.getAttribute(name) === value) continue;
|
|
752
|
+
|
|
753
|
+
// Basic attribute setting
|
|
423
754
|
oldEl.setAttribute(name, value);
|
|
424
|
-
if (name.startsWith("aria-")) {
|
|
425
|
-
const prop = "aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
|
|
426
|
-
oldEl[prop] = value;
|
|
427
|
-
} else if (name.startsWith("data-")) {
|
|
428
|
-
oldEl.dataset[name.slice(5)] = value;
|
|
429
|
-
} else {
|
|
430
|
-
const prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
|
|
431
|
-
if (prop in oldEl) {
|
|
432
|
-
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop);
|
|
433
|
-
const isBoolean = typeof oldEl[prop] === "boolean" || descriptor?.get && typeof descriptor.get.call(oldEl) === "boolean";
|
|
434
|
-
if (isBoolean) {
|
|
435
|
-
oldEl[prop] = value !== "false" && (value === "" || value === prop || value === "true");
|
|
436
|
-
} else {
|
|
437
|
-
oldEl[prop] = value;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
755
|
}
|
|
442
756
|
|
|
443
|
-
// Remove
|
|
757
|
+
// Remove old attributes that are no longer present
|
|
444
758
|
for (let i = oldAttrs.length - 1; i >= 0; i--) {
|
|
445
759
|
const name = oldAttrs[i].name;
|
|
446
760
|
if (!newEl.hasAttribute(name)) {
|
|
@@ -467,12 +781,13 @@ class Renderer {
|
|
|
467
781
|
|
|
468
782
|
/**
|
|
469
783
|
* Creates a key map for the children of a parent node.
|
|
784
|
+
* Used for efficient O(1) lookup of keyed elements during diffing.
|
|
470
785
|
*
|
|
471
786
|
* @private
|
|
472
|
-
* @param {Array<
|
|
787
|
+
* @param {Array<ChildNode>} children - The children of the parent node.
|
|
473
788
|
* @param {number} start - The start index of the children.
|
|
474
789
|
* @param {number} end - The end index of the children.
|
|
475
|
-
* @returns {
|
|
790
|
+
* @returns {KeyMap} A key map for the children.
|
|
476
791
|
*/
|
|
477
792
|
_createKeyMap(children, start, end) {
|
|
478
793
|
const map = new Map();
|
|
@@ -496,43 +811,129 @@ class Renderer {
|
|
|
496
811
|
}
|
|
497
812
|
}
|
|
498
813
|
|
|
814
|
+
// ============================================================================
|
|
815
|
+
// TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
|
|
816
|
+
// ============================================================================
|
|
817
|
+
|
|
818
|
+
// -----------------------------------------------------------------------------
|
|
819
|
+
// Configuration Types
|
|
820
|
+
// -----------------------------------------------------------------------------
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* @typedef {Object} ElevaConfig
|
|
824
|
+
* @property {boolean} [debug=false]
|
|
825
|
+
* Enable debug mode for verbose logging
|
|
826
|
+
* @property {string} [prefix='e']
|
|
827
|
+
* Prefix for component style scoping
|
|
828
|
+
* @property {boolean} [async=true]
|
|
829
|
+
* Enable async component setup
|
|
830
|
+
*/
|
|
831
|
+
|
|
832
|
+
// -----------------------------------------------------------------------------
|
|
833
|
+
// Component Types
|
|
834
|
+
// -----------------------------------------------------------------------------
|
|
835
|
+
|
|
499
836
|
/**
|
|
500
837
|
* @typedef {Object} ComponentDefinition
|
|
501
|
-
* @property {
|
|
838
|
+
* @property {SetupFunction} [setup]
|
|
502
839
|
* Optional setup function that initializes the component's state and returns reactive data
|
|
503
|
-
* @property {
|
|
504
|
-
* Required function that defines the component's HTML structure
|
|
505
|
-
* @property {
|
|
840
|
+
* @property {TemplateFunction|string} template
|
|
841
|
+
* Required function or string that defines the component's HTML structure
|
|
842
|
+
* @property {StyleFunction|string} [style]
|
|
506
843
|
* Optional function or string that provides component-scoped CSS styles
|
|
507
|
-
* @property {
|
|
844
|
+
* @property {ChildrenMap} [children]
|
|
508
845
|
* Optional object defining nested child components
|
|
509
846
|
*/
|
|
510
847
|
|
|
848
|
+
/**
|
|
849
|
+
* @callback SetupFunction
|
|
850
|
+
* @param {ComponentContext} ctx - The component context with props, emitter, and signal factory
|
|
851
|
+
* @returns {SetupResult|Promise<SetupResult>} Reactive data and lifecycle hooks
|
|
852
|
+
*/
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
|
|
856
|
+
* Data returned from setup function, may include lifecycle hooks
|
|
857
|
+
*/
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* @callback TemplateFunction
|
|
861
|
+
* @param {ComponentContext} ctx - The component context
|
|
862
|
+
* @returns {string|Promise<string>} HTML template string
|
|
863
|
+
*/
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* @callback StyleFunction
|
|
867
|
+
* @param {ComponentContext} ctx - The component context
|
|
868
|
+
* @returns {string} CSS styles string
|
|
869
|
+
*/
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* @typedef {Record<string, ComponentDefinition|string>} ChildrenMap
|
|
873
|
+
* Map of CSS selectors to component definitions or registered component names
|
|
874
|
+
*/
|
|
875
|
+
|
|
876
|
+
// -----------------------------------------------------------------------------
|
|
877
|
+
// Context Types
|
|
878
|
+
// -----------------------------------------------------------------------------
|
|
879
|
+
|
|
511
880
|
/**
|
|
512
881
|
* @typedef {Object} ComponentContext
|
|
513
|
-
* @property {
|
|
882
|
+
* @property {ComponentProps} props
|
|
514
883
|
* Component properties passed during mounting
|
|
515
884
|
* @property {Emitter} emitter
|
|
516
885
|
* Event emitter instance for component event handling
|
|
517
|
-
* @property {
|
|
886
|
+
* @property {SignalFactory} signal
|
|
518
887
|
* Factory function to create reactive Signal instances
|
|
519
|
-
|
|
888
|
+
*/
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @typedef {Record<string, unknown>} ComponentProps
|
|
892
|
+
* Properties passed to a component during mounting
|
|
893
|
+
*/
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* @callback SignalFactory
|
|
897
|
+
* @template T
|
|
898
|
+
* @param {T} initialValue - The initial value for the signal
|
|
899
|
+
* @returns {Signal<T>} A new Signal instance
|
|
900
|
+
*/
|
|
901
|
+
|
|
902
|
+
// -----------------------------------------------------------------------------
|
|
903
|
+
// Lifecycle Hook Types
|
|
904
|
+
// -----------------------------------------------------------------------------
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @typedef {Object} LifecycleHooks
|
|
908
|
+
* @property {LifecycleHook} [onBeforeMount]
|
|
520
909
|
* Hook called before component mounting
|
|
521
|
-
* @property {
|
|
910
|
+
* @property {LifecycleHook} [onMount]
|
|
522
911
|
* Hook called after component mounting
|
|
523
|
-
* @property {
|
|
912
|
+
* @property {LifecycleHook} [onBeforeUpdate]
|
|
524
913
|
* Hook called before component update
|
|
525
|
-
* @property {
|
|
914
|
+
* @property {LifecycleHook} [onUpdate]
|
|
526
915
|
* Hook called after component update
|
|
527
|
-
* @property {
|
|
916
|
+
* @property {UnmountHook} [onUnmount]
|
|
528
917
|
* Hook called during component unmounting
|
|
529
918
|
*/
|
|
530
919
|
|
|
920
|
+
/**
|
|
921
|
+
* @callback LifecycleHook
|
|
922
|
+
* @param {LifecycleHookContext} ctx - Context with container and component data
|
|
923
|
+
* @returns {void|Promise<void>}
|
|
924
|
+
*/
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* @callback UnmountHook
|
|
928
|
+
* @param {UnmountHookContext} ctx - Context with cleanup resources
|
|
929
|
+
* @returns {void|Promise<void>}
|
|
930
|
+
*/
|
|
931
|
+
|
|
531
932
|
/**
|
|
532
933
|
* @typedef {Object} LifecycleHookContext
|
|
533
934
|
* @property {HTMLElement} container
|
|
534
935
|
* The DOM element where the component is mounted
|
|
535
|
-
* @property {ComponentContext} context
|
|
936
|
+
* @property {ComponentContext & SetupResult} context
|
|
536
937
|
* The component's reactive state and context data
|
|
537
938
|
*/
|
|
538
939
|
|
|
@@ -540,32 +941,91 @@ class Renderer {
|
|
|
540
941
|
* @typedef {Object} UnmountHookContext
|
|
541
942
|
* @property {HTMLElement} container
|
|
542
943
|
* The DOM element where the component is mounted
|
|
543
|
-
* @property {ComponentContext} context
|
|
944
|
+
* @property {ComponentContext & SetupResult} context
|
|
544
945
|
* The component's reactive state and context data
|
|
545
|
-
* @property {
|
|
546
|
-
* watchers: Array<() => void>, // Signal watcher cleanup functions
|
|
547
|
-
* listeners: Array<() => void>, // Event listener cleanup functions
|
|
548
|
-
* children: Array<MountResult> // Child component instances
|
|
549
|
-
* }} cleanup
|
|
946
|
+
* @property {CleanupResources} cleanup
|
|
550
947
|
* Object containing cleanup functions and instances
|
|
551
948
|
*/
|
|
552
949
|
|
|
950
|
+
/**
|
|
951
|
+
* @typedef {Object} CleanupResources
|
|
952
|
+
* @property {Array<UnsubscribeFunction>} watchers
|
|
953
|
+
* Signal watcher cleanup functions
|
|
954
|
+
* @property {Array<UnsubscribeFunction>} listeners
|
|
955
|
+
* Event listener cleanup functions
|
|
956
|
+
* @property {Array<MountResult>} children
|
|
957
|
+
* Child component instances
|
|
958
|
+
*/
|
|
959
|
+
|
|
960
|
+
// -----------------------------------------------------------------------------
|
|
961
|
+
// Mount Result Types
|
|
962
|
+
// -----------------------------------------------------------------------------
|
|
963
|
+
|
|
553
964
|
/**
|
|
554
965
|
* @typedef {Object} MountResult
|
|
555
966
|
* @property {HTMLElement} container
|
|
556
967
|
* The DOM element where the component is mounted
|
|
557
|
-
* @property {ComponentContext} data
|
|
968
|
+
* @property {ComponentContext & SetupResult} data
|
|
558
969
|
* The component's reactive state and context data
|
|
559
|
-
* @property {
|
|
970
|
+
* @property {UnmountFunction} unmount
|
|
560
971
|
* Function to clean up and unmount the component
|
|
561
972
|
*/
|
|
562
973
|
|
|
974
|
+
/**
|
|
975
|
+
* @callback UnmountFunction
|
|
976
|
+
* @returns {Promise<void>}
|
|
977
|
+
*/
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* @callback UnsubscribeFunction
|
|
981
|
+
* @returns {void|boolean}
|
|
982
|
+
*/
|
|
983
|
+
|
|
984
|
+
// -----------------------------------------------------------------------------
|
|
985
|
+
// Plugin Types
|
|
986
|
+
// -----------------------------------------------------------------------------
|
|
987
|
+
|
|
563
988
|
/**
|
|
564
989
|
* @typedef {Object} ElevaPlugin
|
|
565
|
-
* @property {
|
|
990
|
+
* @property {PluginInstallFunction} install
|
|
566
991
|
* Function that installs the plugin into the Eleva instance
|
|
567
992
|
* @property {string} name
|
|
568
993
|
* Unique identifier name for the plugin
|
|
994
|
+
* @property {PluginUninstallFunction} [uninstall]
|
|
995
|
+
* Optional function to uninstall the plugin
|
|
996
|
+
*/
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* @callback PluginInstallFunction
|
|
1000
|
+
* @param {Eleva} eleva - The Eleva instance
|
|
1001
|
+
* @param {PluginOptions} options - Plugin configuration options
|
|
1002
|
+
* @returns {void|Eleva|unknown} Optionally returns the Eleva instance or plugin result
|
|
1003
|
+
*/
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* @callback PluginUninstallFunction
|
|
1007
|
+
* @param {Eleva} eleva - The Eleva instance
|
|
1008
|
+
* @returns {void}
|
|
1009
|
+
*/
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* @typedef {Record<string, unknown>} PluginOptions
|
|
1013
|
+
* Configuration options passed to a plugin during installation
|
|
1014
|
+
*/
|
|
1015
|
+
|
|
1016
|
+
// -----------------------------------------------------------------------------
|
|
1017
|
+
// Event Types
|
|
1018
|
+
// -----------------------------------------------------------------------------
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* @callback EventHandler
|
|
1022
|
+
* @param {Event} event - The DOM event object
|
|
1023
|
+
* @returns {void}
|
|
1024
|
+
*/
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* @typedef {'click'|'submit'|'input'|'change'|'focus'|'blur'|'keydown'|'keyup'|'keypress'|'mouseenter'|'mouseleave'|'mouseover'|'mouseout'|'mousedown'|'mouseup'|'touchstart'|'touchend'|'touchmove'|'scroll'|'resize'|'load'|'error'|string} DOMEventName
|
|
1028
|
+
* Common DOM event names (prefixed with @ in templates)
|
|
569
1029
|
*/
|
|
570
1030
|
|
|
571
1031
|
/**
|
|
@@ -625,6 +1085,8 @@ class Eleva {
|
|
|
625
1085
|
this.emitter = new Emitter();
|
|
626
1086
|
/** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
|
|
627
1087
|
this.signal = Signal;
|
|
1088
|
+
/** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */
|
|
1089
|
+
this.templateEngine = TemplateEngine;
|
|
628
1090
|
/** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
|
|
629
1091
|
this.renderer = new Renderer();
|
|
630
1092
|
|
|
@@ -632,8 +1094,8 @@ class Eleva {
|
|
|
632
1094
|
this._components = new Map();
|
|
633
1095
|
/** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
|
|
634
1096
|
this._plugins = new Map();
|
|
635
|
-
/** @private {
|
|
636
|
-
this.
|
|
1097
|
+
/** @private {number} Counter for generating unique component IDs */
|
|
1098
|
+
this._componentCounter = 0;
|
|
637
1099
|
}
|
|
638
1100
|
|
|
639
1101
|
/**
|
|
@@ -641,17 +1103,28 @@ class Eleva {
|
|
|
641
1103
|
* The plugin's install function will be called with the Eleva instance and provided options.
|
|
642
1104
|
* After installation, the plugin will be available for use by components.
|
|
643
1105
|
*
|
|
1106
|
+
* Note: Plugins that wrap core methods (e.g., mount) must be uninstalled in reverse order
|
|
1107
|
+
* of installation (LIFO - Last In, First Out) to avoid conflicts.
|
|
1108
|
+
*
|
|
644
1109
|
* @public
|
|
645
1110
|
* @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
|
|
646
1111
|
* @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
|
|
647
1112
|
* @returns {Eleva} The Eleva instance (for method chaining).
|
|
648
1113
|
* @example
|
|
649
1114
|
* app.use(myPlugin, { option1: "value1" });
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* // Correct uninstall order (LIFO)
|
|
1118
|
+
* app.use(PluginA);
|
|
1119
|
+
* app.use(PluginB);
|
|
1120
|
+
* // Uninstall in reverse order:
|
|
1121
|
+
* PluginB.uninstall(app);
|
|
1122
|
+
* PluginA.uninstall(app);
|
|
650
1123
|
*/
|
|
651
1124
|
use(plugin, options = {}) {
|
|
652
|
-
plugin.install(this, options);
|
|
653
1125
|
this._plugins.set(plugin.name, plugin);
|
|
654
|
-
|
|
1126
|
+
const result = plugin.install(this, options);
|
|
1127
|
+
return result !== undefined ? result : this;
|
|
655
1128
|
}
|
|
656
1129
|
|
|
657
1130
|
/**
|
|
@@ -702,6 +1175,9 @@ class Eleva {
|
|
|
702
1175
|
const definition = typeof compName === "string" ? this._components.get(compName) : compName;
|
|
703
1176
|
if (!definition) throw new Error(`Component "${compName}" not registered.`);
|
|
704
1177
|
|
|
1178
|
+
/** @type {string} */
|
|
1179
|
+
const compId = `c${++this._componentCounter}`;
|
|
1180
|
+
|
|
705
1181
|
/**
|
|
706
1182
|
* Destructure the component definition to access core functionality.
|
|
707
1183
|
* - setup: Optional function for component initialization and state management
|
|
@@ -750,44 +1226,66 @@ class Eleva {
|
|
|
750
1226
|
const childInstances = [];
|
|
751
1227
|
/** @type {Array<() => void>} */
|
|
752
1228
|
const listeners = [];
|
|
1229
|
+
/** @private {boolean} Local mounted state for this component instance */
|
|
1230
|
+
let isMounted = false;
|
|
753
1231
|
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1232
|
+
// ========================================================================
|
|
1233
|
+
// Render Batching
|
|
1234
|
+
// ========================================================================
|
|
1235
|
+
|
|
1236
|
+
/** @private {boolean} Flag to prevent multiple queued renders */
|
|
1237
|
+
let renderScheduled = false;
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Schedules a batched render on the next microtask.
|
|
1241
|
+
* Multiple signal changes within the same synchronous block are collapsed into one render.
|
|
1242
|
+
* @private
|
|
1243
|
+
*/
|
|
1244
|
+
const scheduleRender = () => {
|
|
1245
|
+
if (renderScheduled) return;
|
|
1246
|
+
renderScheduled = true;
|
|
1247
|
+
queueMicrotask(async () => {
|
|
1248
|
+
renderScheduled = false;
|
|
1249
|
+
await render();
|
|
766
1250
|
});
|
|
767
|
-
}
|
|
1251
|
+
};
|
|
768
1252
|
|
|
769
1253
|
/**
|
|
770
1254
|
* Renders the component by:
|
|
771
|
-
* 1.
|
|
772
|
-
* 2.
|
|
773
|
-
* 3.
|
|
1255
|
+
* 1. Executing lifecycle hooks
|
|
1256
|
+
* 2. Processing the template
|
|
1257
|
+
* 3. Updating the DOM
|
|
1258
|
+
* 4. Processing events, injecting styles, and mounting child components.
|
|
774
1259
|
*/
|
|
775
1260
|
const render = async () => {
|
|
776
1261
|
const templateResult = typeof template === "function" ? await template(mergedContext) : template;
|
|
777
|
-
const
|
|
778
|
-
|
|
1262
|
+
const html = this.templateEngine.parse(templateResult, mergedContext);
|
|
1263
|
+
|
|
1264
|
+
// Execute before hooks
|
|
1265
|
+
if (!isMounted) {
|
|
1266
|
+
await mergedContext.onBeforeMount?.({
|
|
1267
|
+
container,
|
|
1268
|
+
context: mergedContext
|
|
1269
|
+
});
|
|
1270
|
+
} else {
|
|
1271
|
+
await mergedContext.onBeforeUpdate?.({
|
|
1272
|
+
container,
|
|
1273
|
+
context: mergedContext
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
this.renderer.patchDOM(container, html);
|
|
779
1277
|
this._processEvents(container, mergedContext, listeners);
|
|
780
|
-
if (style) this._injectStyles(container,
|
|
1278
|
+
if (style) this._injectStyles(container, compId, style, mergedContext);
|
|
781
1279
|
if (children) await this._mountComponents(container, children, childInstances);
|
|
782
|
-
|
|
783
|
-
|
|
1280
|
+
|
|
1281
|
+
// Execute after hooks
|
|
1282
|
+
if (!isMounted) {
|
|
784
1283
|
await mergedContext.onMount?.({
|
|
785
1284
|
container,
|
|
786
1285
|
context: mergedContext
|
|
787
1286
|
});
|
|
788
|
-
|
|
1287
|
+
isMounted = true;
|
|
789
1288
|
} else {
|
|
790
|
-
/** @type {LifecycleHookContext} */
|
|
791
1289
|
await mergedContext.onUpdate?.({
|
|
792
1290
|
container,
|
|
793
1291
|
context: mergedContext
|
|
@@ -797,11 +1295,12 @@ class Eleva {
|
|
|
797
1295
|
|
|
798
1296
|
/**
|
|
799
1297
|
* Sets up reactive watchers for all Signal instances in the component's data.
|
|
800
|
-
* When a Signal's value changes,
|
|
1298
|
+
* When a Signal's value changes, a batched render is scheduled.
|
|
1299
|
+
* Multiple changes within the same frame are collapsed into one render.
|
|
801
1300
|
* Stores unsubscribe functions to clean up watchers when component unmounts.
|
|
802
1301
|
*/
|
|
803
1302
|
for (const val of Object.values(data)) {
|
|
804
|
-
if (val instanceof Signal) watchers.push(val.watch(
|
|
1303
|
+
if (val instanceof Signal) watchers.push(val.watch(scheduleRender));
|
|
805
1304
|
}
|
|
806
1305
|
await render();
|
|
807
1306
|
const instance = {
|
|
@@ -865,7 +1364,7 @@ class Eleva {
|
|
|
865
1364
|
/** @type {string} */
|
|
866
1365
|
const handlerName = attr.value;
|
|
867
1366
|
/** @type {(event: Event) => void} */
|
|
868
|
-
const handler = context[handlerName] ||
|
|
1367
|
+
const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
|
|
869
1368
|
if (typeof handler === "function") {
|
|
870
1369
|
el.addEventListener(event, handler);
|
|
871
1370
|
el.removeAttribute(attr.name);
|
|
@@ -881,20 +1380,21 @@ class Eleva {
|
|
|
881
1380
|
*
|
|
882
1381
|
* @private
|
|
883
1382
|
* @param {HTMLElement} container - The container element where styles should be injected.
|
|
884
|
-
* @param {string}
|
|
1383
|
+
* @param {string} compId - The component ID used to identify the style element.
|
|
885
1384
|
* @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
|
|
886
1385
|
* @param {ComponentContext} context - The current component context for style interpolation.
|
|
887
1386
|
* @returns {void}
|
|
888
1387
|
*/
|
|
889
|
-
_injectStyles(container,
|
|
1388
|
+
_injectStyles(container, compId, styleDef, context) {
|
|
890
1389
|
/** @type {string} */
|
|
891
|
-
const newStyle = typeof styleDef === "function" ?
|
|
1390
|
+
const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
|
|
1391
|
+
|
|
892
1392
|
/** @type {HTMLStyleElement|null} */
|
|
893
|
-
let styleEl = container.querySelector(`style[data-e-style="${
|
|
1393
|
+
let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
|
|
894
1394
|
if (styleEl && styleEl.textContent === newStyle) return;
|
|
895
1395
|
if (!styleEl) {
|
|
896
1396
|
styleEl = document.createElement("style");
|
|
897
|
-
styleEl.setAttribute("data-e-style",
|
|
1397
|
+
styleEl.setAttribute("data-e-style", compId);
|
|
898
1398
|
container.appendChild(styleEl);
|
|
899
1399
|
}
|
|
900
1400
|
styleEl.textContent = newStyle;
|
|
@@ -906,21 +1406,20 @@ class Eleva {
|
|
|
906
1406
|
*
|
|
907
1407
|
* @private
|
|
908
1408
|
* @param {HTMLElement} element - The DOM element to extract props from
|
|
909
|
-
* @param {string} prefix - The prefix to look for in attributes
|
|
910
1409
|
* @returns {Record<string, string>} An object containing the extracted props
|
|
911
1410
|
* @example
|
|
912
1411
|
* // For an element with attributes:
|
|
913
1412
|
* // <div :name="John" :age="25">
|
|
914
1413
|
* // Returns: { name: "John", age: "25" }
|
|
915
1414
|
*/
|
|
916
|
-
_extractProps(element
|
|
1415
|
+
_extractProps(element) {
|
|
917
1416
|
if (!element.attributes) return {};
|
|
918
1417
|
const props = {};
|
|
919
1418
|
const attrs = element.attributes;
|
|
920
1419
|
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
921
1420
|
const attr = attrs[i];
|
|
922
|
-
if (attr.name.startsWith(
|
|
923
|
-
const propName = attr.name.slice(
|
|
1421
|
+
if (attr.name.startsWith(":")) {
|
|
1422
|
+
const propName = attr.name.slice(1);
|
|
924
1423
|
props[propName] = attr.value;
|
|
925
1424
|
element.removeAttribute(attr.name);
|
|
926
1425
|
}
|
|
@@ -955,7 +1454,7 @@ class Eleva {
|
|
|
955
1454
|
for (const el of container.querySelectorAll(selector)) {
|
|
956
1455
|
if (!(el instanceof HTMLElement)) continue;
|
|
957
1456
|
/** @type {Record<string, string>} */
|
|
958
|
-
const props = this._extractProps(el
|
|
1457
|
+
const props = this._extractProps(el);
|
|
959
1458
|
/** @type {MountResult} */
|
|
960
1459
|
const instance = await this.mount(el, component, props);
|
|
961
1460
|
if (instance && !childInstances.includes(instance)) {
|