eleva 1.0.1 → 1.1.0
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 +21 -10
- package/dist/{eleva-plugins.cjs.js → eleva-plugins.cjs} +1002 -292
- package/dist/eleva-plugins.cjs.map +1 -0
- package/dist/eleva-plugins.d.cts +1352 -0
- package/dist/eleva-plugins.d.cts.map +1 -0
- package/dist/eleva-plugins.d.ts +1352 -0
- package/dist/eleva-plugins.d.ts.map +1 -0
- package/dist/{eleva-plugins.esm.js → eleva-plugins.js} +1002 -292
- package/dist/eleva-plugins.js.map +1 -0
- package/dist/eleva-plugins.umd.js +1001 -291
- package/dist/eleva-plugins.umd.js.map +1 -1
- package/dist/eleva-plugins.umd.min.js +1 -1
- package/dist/eleva-plugins.umd.min.js.map +1 -1
- package/dist/{eleva.cjs.js → eleva.cjs} +421 -191
- package/dist/eleva.cjs.map +1 -0
- package/dist/eleva.d.cts +1329 -0
- package/dist/eleva.d.cts.map +1 -0
- package/dist/eleva.d.ts +473 -226
- package/dist/eleva.d.ts.map +1 -0
- package/dist/{eleva.esm.js → eleva.js} +422 -192
- package/dist/eleva.js.map +1 -0
- package/dist/eleva.umd.js +420 -190
- package/dist/eleva.umd.js.map +1 -1
- package/dist/eleva.umd.min.js +1 -1
- package/dist/eleva.umd.min.js.map +1 -1
- package/dist/plugins/attr.cjs +279 -0
- package/dist/plugins/attr.cjs.map +1 -0
- package/dist/plugins/attr.d.cts +101 -0
- package/dist/plugins/attr.d.cts.map +1 -0
- package/dist/plugins/attr.d.ts +101 -0
- package/dist/plugins/attr.d.ts.map +1 -0
- package/dist/plugins/attr.js +276 -0
- package/dist/plugins/attr.js.map +1 -0
- package/dist/plugins/attr.umd.js +111 -22
- package/dist/plugins/attr.umd.js.map +1 -1
- package/dist/plugins/attr.umd.min.js +1 -1
- package/dist/plugins/attr.umd.min.js.map +1 -1
- package/dist/plugins/router.cjs +1873 -0
- package/dist/plugins/router.cjs.map +1 -0
- package/dist/plugins/router.d.cts +1296 -0
- package/dist/plugins/router.d.cts.map +1 -0
- package/dist/plugins/router.d.ts +1296 -0
- package/dist/plugins/router.d.ts.map +1 -0
- package/dist/plugins/router.js +1870 -0
- package/dist/plugins/router.js.map +1 -0
- package/dist/plugins/router.umd.js +482 -186
- package/dist/plugins/router.umd.js.map +1 -1
- package/dist/plugins/router.umd.min.js +1 -1
- package/dist/plugins/router.umd.min.js.map +1 -1
- package/dist/plugins/store.cjs +920 -0
- package/dist/plugins/store.cjs.map +1 -0
- package/dist/plugins/store.d.cts +266 -0
- package/dist/plugins/store.d.cts.map +1 -0
- package/dist/plugins/store.d.ts +266 -0
- package/dist/plugins/store.d.ts.map +1 -0
- package/dist/plugins/store.js +917 -0
- package/dist/plugins/store.js.map +1 -0
- package/dist/plugins/store.umd.js +410 -85
- package/dist/plugins/store.umd.js.map +1 -1
- package/dist/plugins/store.umd.min.js +1 -1
- package/dist/plugins/store.umd.min.js.map +1 -1
- package/package.json +112 -68
- package/src/core/Eleva.js +195 -115
- package/src/index.cjs +10 -0
- package/src/index.js +11 -0
- package/src/modules/Emitter.js +68 -20
- package/src/modules/Renderer.js +82 -20
- package/src/modules/Signal.js +43 -15
- package/src/modules/TemplateEngine.js +50 -9
- package/src/plugins/Attr.js +121 -19
- package/src/plugins/Router.js +526 -181
- package/src/plugins/Store.js +448 -69
- package/src/plugins/index.js +1 -0
- package/types/core/Eleva.d.ts +263 -169
- package/types/core/Eleva.d.ts.map +1 -1
- package/types/index.d.cts +3 -0
- package/types/index.d.cts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -1
- package/types/modules/Emitter.d.ts +73 -30
- package/types/modules/Emitter.d.ts.map +1 -1
- package/types/modules/Renderer.d.ts +48 -18
- package/types/modules/Renderer.d.ts.map +1 -1
- package/types/modules/Signal.d.ts +44 -16
- package/types/modules/Signal.d.ts.map +1 -1
- package/types/modules/TemplateEngine.d.ts +46 -11
- package/types/modules/TemplateEngine.d.ts.map +1 -1
- package/types/plugins/Attr.d.ts +83 -16
- package/types/plugins/Attr.d.ts.map +1 -1
- package/types/plugins/Router.d.ts +498 -207
- package/types/plugins/Router.d.ts.map +1 -1
- package/types/plugins/Store.d.ts +211 -37
- package/types/plugins/Store.d.ts.map +1 -1
- package/dist/eleva-plugins.cjs.js.map +0 -1
- package/dist/eleva-plugins.esm.js.map +0 -1
- package/dist/eleva.cjs.js.map +0 -1
- package/dist/eleva.esm.js.map +0 -1
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
/*! Eleva v1.0
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/*! Eleva v1.1.0 | MIT License | https://elevajs.com */
|
|
2
|
+
/**
|
|
3
|
+
* @module eleva/template-engine
|
|
4
|
+
* @fileoverview Expression evaluator for directive attributes and property bindings.
|
|
5
|
+
*/ // ============================================================================
|
|
6
|
+
// TYPE DEFINITIONS
|
|
4
7
|
// ============================================================================
|
|
8
|
+
// -----------------------------------------------------------------------------
|
|
9
|
+
// Data Types
|
|
10
|
+
// -----------------------------------------------------------------------------
|
|
5
11
|
/**
|
|
12
|
+
* Data context object for expression evaluation.
|
|
6
13
|
* @typedef {Record<string, unknown>} ContextData
|
|
7
|
-
*
|
|
14
|
+
* @description Contains variables and functions available during template evaluation.
|
|
8
15
|
*/ /**
|
|
16
|
+
* JavaScript expression string to be evaluated.
|
|
9
17
|
* @typedef {string} Expression
|
|
10
|
-
*
|
|
18
|
+
* @description A JavaScript expression evaluated against a ContextData object.
|
|
11
19
|
*/ /**
|
|
20
|
+
* Result of evaluating an expression.
|
|
12
21
|
* @typedef {unknown} EvaluationResult
|
|
13
|
-
*
|
|
22
|
+
* @description Can be string, number, boolean, object, function, or any JavaScript value.
|
|
23
|
+
*/ // -----------------------------------------------------------------------------
|
|
24
|
+
// Function Types
|
|
25
|
+
// -----------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Compiled expression function cached for performance.
|
|
28
|
+
* @typedef {(data: ContextData) => EvaluationResult} CompiledExpressionFunction
|
|
29
|
+
* @description Pre-compiled function that evaluates an expression against context data.
|
|
14
30
|
*/ /**
|
|
15
31
|
* @class 🔒 TemplateEngine
|
|
16
32
|
* @classdesc A minimal expression evaluator for Eleva's directive attributes.
|
|
@@ -42,16 +58,29 @@
|
|
|
42
58
|
/**
|
|
43
59
|
* Evaluates an expression in the context of the provided data object.
|
|
44
60
|
* Used for resolving `@event` handlers and `:prop` bindings.
|
|
61
|
+
* Non-string expressions are returned as-is.
|
|
62
|
+
*
|
|
63
|
+
* @security CRITICAL SECURITY WARNING
|
|
64
|
+
* This method is NOT sandboxed. It uses `new Function()` and `with` statement,
|
|
65
|
+
* allowing full access to the global scope. Potential attack vectors include:
|
|
66
|
+
* - Code injection via malicious expressions
|
|
67
|
+
* - XSS attacks if user input is used as expressions
|
|
68
|
+
* - Access to sensitive globals (window, document, fetch, etc.)
|
|
69
|
+
*
|
|
70
|
+
* ONLY use with developer-defined template strings.
|
|
71
|
+
* NEVER use with user-provided input or untrusted data.
|
|
45
72
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
73
|
+
* Mitigation strategies:
|
|
74
|
+
* - Always sanitize any user-generated content before rendering in templates
|
|
75
|
+
* - Use Content Security Policy (CSP) headers to restrict script execution
|
|
76
|
+
* - Keep expressions simple (property access, method calls) - avoid complex logic
|
|
49
77
|
*
|
|
50
78
|
* @public
|
|
51
79
|
* @static
|
|
52
|
-
* @param {Expression|unknown} expression - The expression to evaluate.
|
|
80
|
+
* @param {Expression | unknown} expression - The expression to evaluate.
|
|
53
81
|
* @param {ContextData} data - The data context for evaluation.
|
|
54
82
|
* @returns {EvaluationResult} The result of the evaluation, or empty string if evaluation fails.
|
|
83
|
+
* @note Evaluation failures return an empty string without throwing.
|
|
55
84
|
*
|
|
56
85
|
* @example
|
|
57
86
|
* // Property access
|
|
@@ -104,28 +133,51 @@
|
|
|
104
133
|
/**
|
|
105
134
|
* Cache for compiled expression functions.
|
|
106
135
|
* Stores compiled Function objects keyed by expression string for O(1) lookup.
|
|
136
|
+
* The cache persists for the application lifetime and is never cleared.
|
|
137
|
+
* This improves performance for repeated evaluations of the same expression.
|
|
138
|
+
*
|
|
139
|
+
* Memory consideration: For applications with highly dynamic expressions
|
|
140
|
+
* (e.g., user-generated), memory usage grows unbounded. This is typically
|
|
141
|
+
* not an issue for static templates where expressions are finite.
|
|
107
142
|
*
|
|
108
143
|
* @static
|
|
109
144
|
* @private
|
|
110
|
-
* @type {Map<string,
|
|
145
|
+
* @type {Map<string, CompiledExpressionFunction>}
|
|
111
146
|
*/ TemplateEngine._functionCache = new Map();
|
|
112
147
|
|
|
148
|
+
/**
|
|
149
|
+
* @module eleva/signal
|
|
150
|
+
* @fileoverview Reactive Signal primitive for fine-grained state management and change notification.
|
|
151
|
+
*/ // ============================================================================
|
|
152
|
+
// TYPE DEFINITIONS
|
|
113
153
|
// ============================================================================
|
|
114
|
-
//
|
|
115
|
-
//
|
|
154
|
+
// -----------------------------------------------------------------------------
|
|
155
|
+
// Callback Types
|
|
156
|
+
// -----------------------------------------------------------------------------
|
|
116
157
|
/**
|
|
117
|
-
*
|
|
158
|
+
* Callback function invoked when a signal's value changes.
|
|
159
|
+
* @template T The type of value held by the signal.
|
|
118
160
|
* @callback SignalWatcher
|
|
119
|
-
* @param {T} value
|
|
161
|
+
* @param {T} value
|
|
162
|
+
* The new value of the signal.
|
|
120
163
|
* @returns {void}
|
|
121
164
|
*/ /**
|
|
165
|
+
* Function to unsubscribe a watcher from a signal.
|
|
122
166
|
* @callback SignalUnsubscribe
|
|
123
|
-
* @returns {boolean}
|
|
124
|
-
|
|
125
|
-
*
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
* True if the watcher was successfully removed, false if already removed.
|
|
169
|
+
* Safe to call multiple times (idempotent).
|
|
170
|
+
*/ // -----------------------------------------------------------------------------
|
|
171
|
+
// Interface Types
|
|
172
|
+
// -----------------------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* Interface describing the public API of a Signal.
|
|
175
|
+
* @template T The type of value held by the signal.
|
|
126
176
|
* @typedef {Object} SignalLike
|
|
127
|
-
* @property {T} value
|
|
128
|
-
*
|
|
177
|
+
* @property {T} value
|
|
178
|
+
* The current value of the signal.
|
|
179
|
+
* @property {function(SignalWatcher<T>): SignalUnsubscribe} watch
|
|
180
|
+
* Subscribe to value changes.
|
|
129
181
|
*/ /**
|
|
130
182
|
* @class ⚡ Signal
|
|
131
183
|
* @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
|
|
@@ -135,7 +187,7 @@
|
|
|
135
187
|
* Render batching is handled at the component level, not the signal level.
|
|
136
188
|
* The class is generic, allowing type-safe handling of any value type T.
|
|
137
189
|
*
|
|
138
|
-
* @template T The type of value held by
|
|
190
|
+
* @template T The type of value held by the signal.
|
|
139
191
|
*
|
|
140
192
|
* @example
|
|
141
193
|
* // Basic usage
|
|
@@ -153,7 +205,6 @@
|
|
|
153
205
|
*
|
|
154
206
|
* @example
|
|
155
207
|
* // With objects
|
|
156
|
-
* /** @type {Signal<{x: number, y: number}>} *\/
|
|
157
208
|
* const position = new Signal({ x: 0, y: 0 });
|
|
158
209
|
* position.value = { x: 10, y: 20 }; // Triggers watchers
|
|
159
210
|
*
|
|
@@ -171,6 +222,10 @@
|
|
|
171
222
|
* Sets a new value for the signal and synchronously notifies all registered watchers if the value has changed.
|
|
172
223
|
* Synchronous notification preserves stack traces and ensures immediate value consistency.
|
|
173
224
|
*
|
|
225
|
+
* Uses strict equality (===) for comparison. For objects/arrays, watchers are only notified
|
|
226
|
+
* if the reference changes, not if properties are mutated. To trigger updates with objects,
|
|
227
|
+
* assign a new reference: `signal.value = { ...signal.value, updated: true }`.
|
|
228
|
+
*
|
|
174
229
|
* @public
|
|
175
230
|
* @param {T} newVal - The new value to set.
|
|
176
231
|
* @returns {void}
|
|
@@ -187,6 +242,8 @@
|
|
|
187
242
|
* @public
|
|
188
243
|
* @param {SignalWatcher<T>} fn - The callback function to invoke on value change.
|
|
189
244
|
* @returns {SignalUnsubscribe} A function to unsubscribe the watcher.
|
|
245
|
+
* Returns true if watcher was removed, false if it wasn't registered.
|
|
246
|
+
* Safe to call multiple times (idempotent after first call).
|
|
190
247
|
*
|
|
191
248
|
* @example
|
|
192
249
|
* // Basic watching
|
|
@@ -195,6 +252,7 @@
|
|
|
195
252
|
* @example
|
|
196
253
|
* // Stop watching
|
|
197
254
|
* unsubscribe(); // Returns true if watcher was removed
|
|
255
|
+
* unsubscribe(); // Returns false (already removed, safe to call again)
|
|
198
256
|
*
|
|
199
257
|
* @example
|
|
200
258
|
* // Multiple watchers
|
|
@@ -210,6 +268,9 @@
|
|
|
210
268
|
* This preserves stack traces for debugging and ensures immediate
|
|
211
269
|
* value consistency. Render batching is handled at the component level.
|
|
212
270
|
*
|
|
271
|
+
* @note If a watcher throws, subsequent watchers are NOT called.
|
|
272
|
+
* The error propagates to the caller (the setter).
|
|
273
|
+
*
|
|
213
274
|
* @private
|
|
214
275
|
* @returns {void}
|
|
215
276
|
*/ _notify() {
|
|
@@ -219,6 +280,7 @@
|
|
|
219
280
|
* Creates a new Signal instance with the specified initial value.
|
|
220
281
|
*
|
|
221
282
|
* @public
|
|
283
|
+
* @constructor
|
|
222
284
|
* @param {T} value - The initial value of the signal.
|
|
223
285
|
*
|
|
224
286
|
* @example
|
|
@@ -228,12 +290,9 @@
|
|
|
228
290
|
* const active = new Signal(true); // Signal<boolean>
|
|
229
291
|
*
|
|
230
292
|
* @example
|
|
231
|
-
* // Complex types
|
|
232
|
-
*
|
|
233
|
-
* const
|
|
234
|
-
*
|
|
235
|
-
* /** @type {Signal<{id: number, name: string} | null>} *\/
|
|
236
|
-
* const user = new Signal(null);
|
|
293
|
+
* // Complex types
|
|
294
|
+
* const items = new Signal([]); // Signal holding an array
|
|
295
|
+
* const user = new Signal(null); // Signal holding nullable object
|
|
237
296
|
*/ constructor(value){
|
|
238
297
|
/**
|
|
239
298
|
* Internal storage for the signal's current value.
|
|
@@ -248,25 +307,56 @@
|
|
|
248
307
|
}
|
|
249
308
|
}
|
|
250
309
|
|
|
310
|
+
/**
|
|
311
|
+
* @module eleva/emitter
|
|
312
|
+
* @fileoverview Event emitter for publish-subscribe communication between components.
|
|
313
|
+
*/ // ============================================================================
|
|
314
|
+
// TYPE DEFINITIONS
|
|
251
315
|
// ============================================================================
|
|
252
|
-
//
|
|
253
|
-
//
|
|
316
|
+
// -----------------------------------------------------------------------------
|
|
317
|
+
// Callback Types
|
|
318
|
+
// -----------------------------------------------------------------------------
|
|
254
319
|
/**
|
|
255
|
-
*
|
|
320
|
+
* Callback function invoked when an event is emitted.
|
|
256
321
|
* @callback EventHandler
|
|
257
|
-
* @param {...
|
|
258
|
-
*
|
|
322
|
+
* @param {...any} args
|
|
323
|
+
* Event arguments passed to the handler.
|
|
324
|
+
* @returns {void | Promise<void>}
|
|
259
325
|
*/ /**
|
|
326
|
+
* Function to unsubscribe an event handler.
|
|
260
327
|
* @callback EventUnsubscribe
|
|
261
328
|
* @returns {void}
|
|
262
|
-
*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
329
|
+
*/ // -----------------------------------------------------------------------------
|
|
330
|
+
// Event Types
|
|
331
|
+
// -----------------------------------------------------------------------------
|
|
332
|
+
/**
|
|
333
|
+
* Event name string identifier.
|
|
334
|
+
* @typedef {string} EventName
|
|
335
|
+
* @description
|
|
336
|
+
* Recommended convention: 'namespace:action' (e.g., 'user:login').
|
|
337
|
+
* This pattern prevents naming collisions and improves code readability.
|
|
338
|
+
*
|
|
339
|
+
* Common namespaces:
|
|
340
|
+
* - `user:` - User-related events (login, logout, update)
|
|
341
|
+
* - `component:` - Component lifecycle events (mount, unmount)
|
|
342
|
+
* - `router:` - Navigation events (beforeEach, afterEach)
|
|
343
|
+
* - `store:` - State management events (change, error)
|
|
344
|
+
* @example
|
|
345
|
+
* 'user:login' // User logged in
|
|
346
|
+
* 'cart:update' // Shopping cart updated
|
|
347
|
+
* 'component:mount' // Component was mounted
|
|
348
|
+
*/ // -----------------------------------------------------------------------------
|
|
349
|
+
// Interface Types
|
|
350
|
+
// -----------------------------------------------------------------------------
|
|
351
|
+
/**
|
|
352
|
+
* Interface describing the public API of an Emitter.
|
|
266
353
|
* @typedef {Object} EmitterLike
|
|
267
|
-
* @property {
|
|
268
|
-
*
|
|
269
|
-
* @property {
|
|
354
|
+
* @property {(event: string, handler: EventHandler) => EventUnsubscribe} on
|
|
355
|
+
* Subscribe to an event.
|
|
356
|
+
* @property {(event: string, handler?: EventHandler) => void} off
|
|
357
|
+
* Unsubscribe from an event.
|
|
358
|
+
* @property {(event: string, ...args: unknown[]) => void} emit
|
|
359
|
+
* Emit an event with arguments.
|
|
270
360
|
*/ /**
|
|
271
361
|
* @class 📡 Emitter
|
|
272
362
|
* @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
|
|
@@ -304,6 +394,7 @@
|
|
|
304
394
|
* // Lifecycle events
|
|
305
395
|
* emitter.on('component:mount', (component) => {});
|
|
306
396
|
* emitter.on('component:unmount', (component) => {});
|
|
397
|
+
* // Note: These lifecycle names are conventions; Eleva core does not emit them by default.
|
|
307
398
|
* // State events
|
|
308
399
|
* emitter.on('state:change', (newState, oldState) => {});
|
|
309
400
|
* // Navigation events
|
|
@@ -317,9 +408,10 @@
|
|
|
317
408
|
* Event names should follow the format 'namespace:action' for consistency.
|
|
318
409
|
*
|
|
319
410
|
* @public
|
|
320
|
-
* @template T
|
|
321
411
|
* @param {string} event - The name of the event to listen for (e.g., 'user:login').
|
|
322
|
-
* @param {EventHandler
|
|
412
|
+
* @param {EventHandler} handler - The callback function to invoke when the event occurs.
|
|
413
|
+
* Note: Handlers returning Promises are NOT awaited. For async operations,
|
|
414
|
+
* handle promise resolution within your handler.
|
|
323
415
|
* @returns {EventUnsubscribe} A function to unsubscribe the event handler.
|
|
324
416
|
*
|
|
325
417
|
* @example
|
|
@@ -327,8 +419,8 @@
|
|
|
327
419
|
* const unsubscribe = emitter.on('user:login', (user) => console.log(user));
|
|
328
420
|
*
|
|
329
421
|
* @example
|
|
330
|
-
* //
|
|
331
|
-
* emitter.on('user:update', (
|
|
422
|
+
* // Handler with typed parameter
|
|
423
|
+
* emitter.on('user:update', (user) => {
|
|
332
424
|
* console.log(`User ${user.id}: ${user.name}`);
|
|
333
425
|
* });
|
|
334
426
|
*
|
|
@@ -343,13 +435,15 @@
|
|
|
343
435
|
}
|
|
344
436
|
/**
|
|
345
437
|
* Removes an event handler for the specified event name.
|
|
346
|
-
* If no handler is provided, all handlers for the event are removed.
|
|
347
438
|
* Automatically cleans up empty event sets to prevent memory leaks.
|
|
348
439
|
*
|
|
440
|
+
* Behavior varies based on whether handler is provided:
|
|
441
|
+
* - With handler: Removes only that specific handler function (O(1) Set deletion)
|
|
442
|
+
* - Without handler: Removes ALL handlers for the event (O(1) Map deletion)
|
|
443
|
+
*
|
|
349
444
|
* @public
|
|
350
|
-
* @template T
|
|
351
445
|
* @param {string} event - The name of the event to remove handlers from.
|
|
352
|
-
* @param {EventHandler
|
|
446
|
+
* @param {EventHandler} [handler] - The specific handler to remove. If omitted, all handlers are removed.
|
|
353
447
|
* @returns {void}
|
|
354
448
|
*
|
|
355
449
|
* @example
|
|
@@ -375,12 +469,19 @@
|
|
|
375
469
|
* Emits an event with the specified data to all registered handlers.
|
|
376
470
|
* Handlers are called synchronously in the order they were registered.
|
|
377
471
|
* If no handlers are registered for the event, the emission is silently ignored.
|
|
472
|
+
* Handlers that return promises are not awaited.
|
|
473
|
+
*
|
|
474
|
+
* Error propagation behavior:
|
|
475
|
+
* - If a handler throws synchronously, the error propagates immediately
|
|
476
|
+
* - Remaining handlers in the iteration are NOT called after an error
|
|
477
|
+
* - For error-resilient emission, wrap your emit call in try/catch
|
|
478
|
+
* - Async handler rejections are not caught (fire-and-forget)
|
|
378
479
|
*
|
|
379
480
|
* @public
|
|
380
|
-
* @template T
|
|
381
481
|
* @param {string} event - The name of the event to emit.
|
|
382
|
-
* @param {...
|
|
482
|
+
* @param {...any} args - Optional arguments to pass to the event handlers.
|
|
383
483
|
* @returns {void}
|
|
484
|
+
* @throws {Error} If a handler throws synchronously, the error propagates to the caller.
|
|
384
485
|
*
|
|
385
486
|
* @example
|
|
386
487
|
* // Emit an event with data
|
|
@@ -398,9 +499,10 @@
|
|
|
398
499
|
if (handlers) for (const handler of handlers)handler(...args);
|
|
399
500
|
}
|
|
400
501
|
/**
|
|
401
|
-
* Creates a new Emitter instance.
|
|
502
|
+
* Creates a new Emitter instance with an empty event registry.
|
|
402
503
|
*
|
|
403
504
|
* @public
|
|
505
|
+
* @constructor
|
|
404
506
|
*
|
|
405
507
|
* @example
|
|
406
508
|
* const emitter = new Emitter();
|
|
@@ -408,22 +510,47 @@
|
|
|
408
510
|
/**
|
|
409
511
|
* Map of event names to their registered handler functions
|
|
410
512
|
* @private
|
|
411
|
-
* @type {Map<string, Set<EventHandler
|
|
513
|
+
* @type {Map<string, Set<EventHandler>>}
|
|
412
514
|
*/ this._events = new Map();
|
|
413
515
|
}
|
|
414
516
|
}
|
|
415
517
|
|
|
518
|
+
/**
|
|
519
|
+
* @module eleva/renderer
|
|
520
|
+
* @fileoverview High-performance DOM renderer with two-pointer diffing and keyed reconciliation.
|
|
521
|
+
*/ // ============================================================================
|
|
522
|
+
// TYPE DEFINITIONS
|
|
416
523
|
// ============================================================================
|
|
417
|
-
//
|
|
418
|
-
//
|
|
524
|
+
// -----------------------------------------------------------------------------
|
|
525
|
+
// Data Types
|
|
526
|
+
// -----------------------------------------------------------------------------
|
|
419
527
|
/**
|
|
528
|
+
* Map of key attribute values to their corresponding DOM nodes.
|
|
420
529
|
* @typedef {Map<string, Node>} KeyMap
|
|
421
|
-
*
|
|
422
|
-
*/
|
|
530
|
+
* @description Enables O(1) lookup for keyed element reconciliation.
|
|
531
|
+
*/ // -----------------------------------------------------------------------------
|
|
532
|
+
// Interface Types
|
|
533
|
+
// -----------------------------------------------------------------------------
|
|
534
|
+
/**
|
|
535
|
+
* Interface describing the public API of a Renderer.
|
|
423
536
|
* @typedef {Object} RendererLike
|
|
424
|
-
* @property {function(HTMLElement, string): void} patchDOM
|
|
425
|
-
|
|
426
|
-
*
|
|
537
|
+
* @property {function(HTMLElement, string): void} patchDOM
|
|
538
|
+
* Patches the DOM with new HTML content.
|
|
539
|
+
* @description
|
|
540
|
+
* Plugins may extend renderer behavior by wrapping private methods (e.g., `_patchNode`),
|
|
541
|
+
* but those hooks are not part of the public API.
|
|
542
|
+
*/ // ============================================================================
|
|
543
|
+
// CONSTANTS
|
|
544
|
+
// ============================================================================
|
|
545
|
+
/**
|
|
546
|
+
* Properties that can diverge from attributes after user interaction.
|
|
547
|
+
* These are synchronized during DOM patching to ensure element state
|
|
548
|
+
* matches the rendered HTML attributes.
|
|
549
|
+
*
|
|
550
|
+
* - `value`: Text input, textarea, select element values
|
|
551
|
+
* - `checked`: Checkbox and radio button states
|
|
552
|
+
* - `selected`: Option element selection states
|
|
553
|
+
*
|
|
427
554
|
* @private
|
|
428
555
|
* @type {string[]}
|
|
429
556
|
*/ const SYNC_PROPS = [
|
|
@@ -484,6 +611,9 @@
|
|
|
484
611
|
* @example
|
|
485
612
|
* // Empty the container
|
|
486
613
|
* renderer.patchDOM(container, '');
|
|
614
|
+
*
|
|
615
|
+
* @see _diff - Low-level diffing algorithm.
|
|
616
|
+
* @see _patchNode - Individual node patching.
|
|
487
617
|
*/ patchDOM(container, newHtml) {
|
|
488
618
|
this._tempContainer.innerHTML = newHtml;
|
|
489
619
|
this._diff(container, this._tempContainer);
|
|
@@ -493,16 +623,26 @@
|
|
|
493
623
|
/**
|
|
494
624
|
* Performs a diff between two DOM nodes and patches the old node to match the new node.
|
|
495
625
|
* Uses a two-pointer algorithm with key-based reconciliation for optimal performance.
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
626
|
+
* This method modifies oldParent in-place - it is not a pure function.
|
|
627
|
+
*
|
|
628
|
+
* Algorithm details:
|
|
629
|
+
* 1. Early exit if both nodes have no children (O(1) leaf node optimization)
|
|
630
|
+
* 2. Convert NodeLists to arrays for indexed access
|
|
631
|
+
* 3. Initialize two-pointer indices (oldStart/oldEnd, newStart/newEnd)
|
|
632
|
+
* 4. While pointers haven't crossed:
|
|
633
|
+
* a. Skip null entries (from previous moves)
|
|
634
|
+
* b. If nodes match (same key+tag or same type+name): patch and advance
|
|
635
|
+
* c. On mismatch: lazily build key→node map for O(1) lookup
|
|
636
|
+
* d. If keyed match found: move existing node (preserves DOM identity)
|
|
637
|
+
* e. Otherwise: clone and insert new node
|
|
638
|
+
* 5. After loop: append remaining new nodes or remove remaining old nodes
|
|
639
|
+
*
|
|
640
|
+
* Complexity: O(n) for most cases, O(n²) worst case with no keys.
|
|
641
|
+
* Non-keyed elements are matched by position and tag name.
|
|
502
642
|
*
|
|
503
643
|
* @private
|
|
504
|
-
* @param {
|
|
505
|
-
* @param {
|
|
644
|
+
* @param {Element} oldParent - The original DOM element to update (modified in-place).
|
|
645
|
+
* @param {Element} newParent - The new DOM element with desired state.
|
|
506
646
|
* @returns {void}
|
|
507
647
|
*/ _diff(oldParent, newParent) {
|
|
508
648
|
// Early exit for leaf nodes (no children)
|
|
@@ -561,7 +701,8 @@
|
|
|
561
701
|
}
|
|
562
702
|
/**
|
|
563
703
|
* Patches a single node, updating its content and attributes to match the new node.
|
|
564
|
-
* Handles text nodes
|
|
704
|
+
* Handles text nodes (nodeType 3 / Node.TEXT_NODE) by updating nodeValue,
|
|
705
|
+
* and element nodes (nodeType 1 / Node.ELEMENT_NODE) by updating attributes
|
|
565
706
|
* and recursively diffing children.
|
|
566
707
|
*
|
|
567
708
|
* Skips nodes that are managed by Eleva component instances to prevent interference
|
|
@@ -586,12 +727,20 @@
|
|
|
586
727
|
/**
|
|
587
728
|
* Removes a node from its parent, with special handling for Eleva-managed elements.
|
|
588
729
|
* Style elements with the `data-e-style` attribute are preserved to maintain
|
|
589
|
-
* component
|
|
730
|
+
* component styles across re-renders. Without this protection, component styles
|
|
731
|
+
* would be removed during DOM diffing and lost until the next full re-render.
|
|
732
|
+
*
|
|
733
|
+
* @note Style tags persist for the component's entire lifecycle. If the template
|
|
734
|
+
* conditionally removes elements that the CSS rules target (e.g., `.foo` elements),
|
|
735
|
+
* the style rules remain but simply have no matching elements. This is expected
|
|
736
|
+
* behavior - styles are cleaned up when the component unmounts, not when individual
|
|
737
|
+
* elements are removed.
|
|
590
738
|
*
|
|
591
739
|
* @private
|
|
592
740
|
* @param {HTMLElement} parent - The parent element containing the node.
|
|
593
741
|
* @param {Node} node - The node to remove.
|
|
594
742
|
* @returns {void}
|
|
743
|
+
* @see _injectStyles - Where data-e-style elements are created.
|
|
595
744
|
*/ _removeNode(parent, node) {
|
|
596
745
|
// Preserve Eleva-managed style elements
|
|
597
746
|
if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
|
|
@@ -602,12 +751,16 @@
|
|
|
602
751
|
* Adds new attributes, updates changed values, and removes attributes no longer present.
|
|
603
752
|
* Also syncs DOM properties that can diverge from attributes after user interaction.
|
|
604
753
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
754
|
+
* Processing order:
|
|
755
|
+
* 1. Iterate new attributes, skip @ prefixed (event) attributes
|
|
756
|
+
* 2. Update attribute if value changed
|
|
757
|
+
* 3. Sync corresponding DOM property if writable (handles boolean conversion)
|
|
758
|
+
* 4. Iterate old attributes in reverse, remove if not in new element
|
|
759
|
+
* 5. Sync SYNC_PROPS (value, checked, selected) from new to old element
|
|
607
760
|
*
|
|
608
761
|
* @private
|
|
609
|
-
* @param {
|
|
610
|
-
* @param {
|
|
762
|
+
* @param {Element} oldEl - The original element to update.
|
|
763
|
+
* @param {Element} newEl - The new element with target attributes.
|
|
611
764
|
* @returns {void}
|
|
612
765
|
*/ _updateAttributes(oldEl, newEl) {
|
|
613
766
|
// Add/update attributes from new element
|
|
@@ -668,10 +821,11 @@
|
|
|
668
821
|
/**
|
|
669
822
|
* Extracts the key attribute from a node if it exists.
|
|
670
823
|
* Only element nodes (nodeType === 1) can have key attributes.
|
|
824
|
+
* Uses optional chaining for null-safe access.
|
|
671
825
|
*
|
|
672
826
|
* @private
|
|
673
|
-
* @param {Node|null|undefined} node - The node to extract the key from.
|
|
674
|
-
* @returns {string|null} The key attribute value, or null if not an element or no key.
|
|
827
|
+
* @param {Node | null | undefined} node - The node to extract the key from.
|
|
828
|
+
* @returns {string | null} The key attribute value, or null if not an element or no key.
|
|
675
829
|
*/ _getNodeKey(node) {
|
|
676
830
|
return node?.nodeType === 1 ? node.getAttribute("key") : null;
|
|
677
831
|
}
|
|
@@ -680,7 +834,7 @@
|
|
|
680
834
|
* The map is built lazily only when needed (when a mismatch occurs during diffing).
|
|
681
835
|
*
|
|
682
836
|
* @private
|
|
683
|
-
* @param {
|
|
837
|
+
* @param {ChildNode[]} children - The array of child nodes to map.
|
|
684
838
|
* @param {number} start - The start index (inclusive) for mapping.
|
|
685
839
|
* @param {number} end - The end index (inclusive) for mapping.
|
|
686
840
|
* @returns {KeyMap} A Map of key strings to their corresponding DOM nodes.
|
|
@@ -694,8 +848,13 @@
|
|
|
694
848
|
}
|
|
695
849
|
/**
|
|
696
850
|
* Creates a new Renderer instance.
|
|
851
|
+
* Initializes a reusable temporary container for HTML parsing.
|
|
852
|
+
*
|
|
853
|
+
* Performance: The temp container is reused across all patch operations,
|
|
854
|
+
* minimizing memory allocation overhead (O(1) memory per Renderer instance).
|
|
697
855
|
*
|
|
698
856
|
* @public
|
|
857
|
+
* @constructor
|
|
699
858
|
*
|
|
700
859
|
* @example
|
|
701
860
|
* const renderer = new Renderer();
|
|
@@ -710,168 +869,195 @@
|
|
|
710
869
|
}
|
|
711
870
|
|
|
712
871
|
// ============================================================================
|
|
713
|
-
// TYPE DEFINITIONS
|
|
872
|
+
// TYPE DEFINITIONS
|
|
714
873
|
// ============================================================================
|
|
715
874
|
// -----------------------------------------------------------------------------
|
|
716
875
|
// Configuration Types
|
|
717
876
|
// -----------------------------------------------------------------------------
|
|
718
877
|
/**
|
|
719
|
-
*
|
|
720
|
-
* @
|
|
721
|
-
* Enable debug mode for verbose logging
|
|
722
|
-
* @property {string} [prefix='e']
|
|
723
|
-
* Prefix for component style scoping
|
|
724
|
-
* @property {boolean} [async=true]
|
|
725
|
-
* Enable async component setup
|
|
878
|
+
* Configuration options for the Eleva instance (reserved for future use).
|
|
879
|
+
* @typedef {Record<string, unknown>} ElevaConfig
|
|
726
880
|
*/ // -----------------------------------------------------------------------------
|
|
727
881
|
// Component Types
|
|
728
882
|
// -----------------------------------------------------------------------------
|
|
729
883
|
/**
|
|
884
|
+
* Component definition object.
|
|
730
885
|
* @typedef {Object} ComponentDefinition
|
|
731
886
|
* @property {SetupFunction} [setup]
|
|
732
|
-
* Optional setup function that initializes the component's state and returns reactive data
|
|
733
|
-
* @property {TemplateFunction|string} template
|
|
734
|
-
* Required function or string that defines the component's HTML structure
|
|
735
|
-
* @property {StyleFunction|string} [style]
|
|
736
|
-
* Optional function or string that provides
|
|
887
|
+
* Optional setup function that initializes the component's state and returns reactive data.
|
|
888
|
+
* @property {TemplateFunction | string} template
|
|
889
|
+
* Required function or string that defines the component's HTML structure.
|
|
890
|
+
* @property {StyleFunction | string} [style]
|
|
891
|
+
* Optional function or string that provides CSS styles for the component.
|
|
892
|
+
* Styles are preserved across DOM diffs via data-e-style markers.
|
|
737
893
|
* @property {ChildrenMap} [children]
|
|
738
|
-
* Optional object defining nested child components
|
|
894
|
+
* Optional object defining nested child components.
|
|
739
895
|
*/ /**
|
|
896
|
+
* Setup function that initializes component state.
|
|
740
897
|
* @callback SetupFunction
|
|
741
|
-
* @param {ComponentContext} ctx
|
|
742
|
-
*
|
|
898
|
+
* @param {ComponentContext} ctx
|
|
899
|
+
* The component context with props, emitter, and signal factory.
|
|
900
|
+
* @returns {SetupResult | Promise<SetupResult>}
|
|
901
|
+
* Reactive data and lifecycle hooks.
|
|
743
902
|
*/ /**
|
|
903
|
+
* Data returned from setup function, may include lifecycle hooks.
|
|
744
904
|
* @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
|
|
745
|
-
* Data returned from setup function, may include lifecycle hooks
|
|
746
905
|
*/ /**
|
|
906
|
+
* Template function that returns HTML markup.
|
|
747
907
|
* @callback TemplateFunction
|
|
748
|
-
* @param {ComponentContext} ctx
|
|
749
|
-
*
|
|
908
|
+
* @param {ComponentContext & SetupResult} ctx
|
|
909
|
+
* The merged component context and setup data.
|
|
910
|
+
* @returns {string | Promise<string>}
|
|
911
|
+
* HTML template string.
|
|
750
912
|
*/ /**
|
|
913
|
+
* Style function that returns CSS styles.
|
|
751
914
|
* @callback StyleFunction
|
|
752
|
-
* @param {ComponentContext} ctx
|
|
753
|
-
*
|
|
915
|
+
* @param {ComponentContext & SetupResult} ctx
|
|
916
|
+
* The merged component context and setup data.
|
|
917
|
+
* @returns {string}
|
|
918
|
+
* CSS styles string.
|
|
754
919
|
*/ /**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
920
|
+
* Map of CSS selectors to component definitions or registered component names.
|
|
921
|
+
* @typedef {Record<string, ComponentDefinition | string>} ChildrenMap
|
|
757
922
|
*/ // -----------------------------------------------------------------------------
|
|
758
923
|
// Context Types
|
|
759
924
|
// -----------------------------------------------------------------------------
|
|
760
925
|
/**
|
|
926
|
+
* Context passed to component setup function.
|
|
761
927
|
* @typedef {Object} ComponentContext
|
|
762
928
|
* @property {ComponentProps} props
|
|
763
|
-
* Component properties passed during mounting
|
|
929
|
+
* Component properties passed during mounting.
|
|
764
930
|
* @property {Emitter} emitter
|
|
765
|
-
* Event emitter instance for component event handling
|
|
931
|
+
* Event emitter instance for component event handling.
|
|
766
932
|
* @property {SignalFactory} signal
|
|
767
|
-
* Factory function to create reactive Signal instances
|
|
933
|
+
* Factory function to create reactive Signal instances.
|
|
934
|
+
* @description
|
|
935
|
+
* Plugins may extend this context with additional properties (e.g., `ctx.router`, `ctx.store`).
|
|
936
|
+
* @see RouterContext - Router plugin injected context.
|
|
937
|
+
* @see StoreApi - Store plugin injected context.
|
|
768
938
|
*/ /**
|
|
939
|
+
* Properties passed to a component during mounting.
|
|
769
940
|
* @typedef {Record<string, unknown>} ComponentProps
|
|
770
|
-
* Properties passed to a component during mounting
|
|
771
941
|
*/ /**
|
|
772
|
-
*
|
|
773
|
-
* @
|
|
774
|
-
* @param {T} initialValue - The initial value for the signal
|
|
775
|
-
* @returns {Signal<T>} A new Signal instance
|
|
942
|
+
* Factory function to create reactive Signal instances.
|
|
943
|
+
* @typedef {<T>(initialValue: T) => Signal<T>} SignalFactory
|
|
776
944
|
*/ // -----------------------------------------------------------------------------
|
|
777
945
|
// Lifecycle Hook Types
|
|
778
946
|
// -----------------------------------------------------------------------------
|
|
779
947
|
/**
|
|
948
|
+
* Lifecycle hooks that can be returned from setup function.
|
|
780
949
|
* @typedef {Object} LifecycleHooks
|
|
781
950
|
* @property {LifecycleHook} [onBeforeMount]
|
|
782
|
-
*
|
|
951
|
+
* Called before component mounting.
|
|
783
952
|
* @property {LifecycleHook} [onMount]
|
|
784
|
-
*
|
|
953
|
+
* Called after component mounting.
|
|
785
954
|
* @property {LifecycleHook} [onBeforeUpdate]
|
|
786
|
-
*
|
|
955
|
+
* Called before component update.
|
|
787
956
|
* @property {LifecycleHook} [onUpdate]
|
|
788
|
-
*
|
|
957
|
+
* Called after component update.
|
|
789
958
|
* @property {UnmountHook} [onUnmount]
|
|
790
|
-
*
|
|
959
|
+
* Called during component unmounting.
|
|
791
960
|
*/ /**
|
|
961
|
+
* Lifecycle hook function.
|
|
792
962
|
* @callback LifecycleHook
|
|
793
|
-
* @param {LifecycleHookContext} ctx
|
|
794
|
-
*
|
|
963
|
+
* @param {LifecycleHookContext} ctx
|
|
964
|
+
* Context with container and component data.
|
|
965
|
+
* @returns {void | Promise<void>}
|
|
795
966
|
*/ /**
|
|
967
|
+
* Unmount hook function with cleanup resources.
|
|
796
968
|
* @callback UnmountHook
|
|
797
|
-
* @param {UnmountHookContext} ctx
|
|
798
|
-
*
|
|
969
|
+
* @param {UnmountHookContext} ctx
|
|
970
|
+
* Context with cleanup resources.
|
|
971
|
+
* @returns {void | Promise<void>}
|
|
799
972
|
*/ /**
|
|
973
|
+
* Context passed to lifecycle hooks.
|
|
800
974
|
* @typedef {Object} LifecycleHookContext
|
|
801
975
|
* @property {HTMLElement} container
|
|
802
|
-
* The DOM element where the component is mounted
|
|
976
|
+
* The DOM element where the component is mounted.
|
|
803
977
|
* @property {ComponentContext & SetupResult} context
|
|
804
|
-
* The component's reactive state and context data
|
|
978
|
+
* The component's reactive state and context data.
|
|
805
979
|
*/ /**
|
|
980
|
+
* Context passed to unmount hook with cleanup resources.
|
|
806
981
|
* @typedef {Object} UnmountHookContext
|
|
807
982
|
* @property {HTMLElement} container
|
|
808
|
-
* The DOM element where the component is mounted
|
|
983
|
+
* The DOM element where the component is mounted.
|
|
809
984
|
* @property {ComponentContext & SetupResult} context
|
|
810
|
-
* The component's reactive state and context data
|
|
985
|
+
* The component's reactive state and context data.
|
|
811
986
|
* @property {CleanupResources} cleanup
|
|
812
|
-
* Object containing cleanup functions and instances
|
|
987
|
+
* Object containing cleanup functions and instances.
|
|
813
988
|
*/ /**
|
|
989
|
+
* Resources available for cleanup during unmount.
|
|
814
990
|
* @typedef {Object} CleanupResources
|
|
815
|
-
* @property {
|
|
816
|
-
* Signal watcher cleanup functions
|
|
817
|
-
* @property {
|
|
818
|
-
* Event listener cleanup functions
|
|
819
|
-
* @property {
|
|
820
|
-
* Child component instances
|
|
991
|
+
* @property {UnsubscribeFunction[]} watchers
|
|
992
|
+
* Signal watcher cleanup functions.
|
|
993
|
+
* @property {UnsubscribeFunction[]} listeners
|
|
994
|
+
* Event listener cleanup functions.
|
|
995
|
+
* @property {MountResult[]} children
|
|
996
|
+
* Child component instances.
|
|
821
997
|
*/ // -----------------------------------------------------------------------------
|
|
822
998
|
// Mount Result Types
|
|
823
999
|
// -----------------------------------------------------------------------------
|
|
824
1000
|
/**
|
|
1001
|
+
* Result of mounting a component.
|
|
825
1002
|
* @typedef {Object} MountResult
|
|
826
1003
|
* @property {HTMLElement} container
|
|
827
|
-
* The DOM element where the component is mounted
|
|
1004
|
+
* The DOM element where the component is mounted.
|
|
828
1005
|
* @property {ComponentContext & SetupResult} data
|
|
829
|
-
* The component's reactive state and context data
|
|
1006
|
+
* The component's reactive state and context data.
|
|
830
1007
|
* @property {UnmountFunction} unmount
|
|
831
|
-
* Function to clean up and unmount the component
|
|
1008
|
+
* Function to clean up and unmount the component.
|
|
832
1009
|
*/ /**
|
|
1010
|
+
* Function to unmount a component and clean up resources.
|
|
833
1011
|
* @callback UnmountFunction
|
|
834
1012
|
* @returns {Promise<void>}
|
|
835
1013
|
*/ /**
|
|
1014
|
+
* Function to unsubscribe from events or watchers.
|
|
836
1015
|
* @callback UnsubscribeFunction
|
|
837
|
-
* @returns {void|boolean}
|
|
1016
|
+
* @returns {void | boolean}
|
|
838
1017
|
*/ // -----------------------------------------------------------------------------
|
|
839
1018
|
// Plugin Types
|
|
840
1019
|
// -----------------------------------------------------------------------------
|
|
841
1020
|
/**
|
|
1021
|
+
* Plugin interface for extending Eleva.
|
|
842
1022
|
* @typedef {Object} ElevaPlugin
|
|
843
|
-
* @property {PluginInstallFunction} install
|
|
844
|
-
* Function that installs the plugin into the Eleva instance
|
|
845
1023
|
* @property {string} name
|
|
846
|
-
* Unique identifier name for the plugin
|
|
1024
|
+
* Unique identifier name for the plugin.
|
|
1025
|
+
* @property {string} [version]
|
|
1026
|
+
* Optional version string for the plugin.
|
|
1027
|
+
* @property {PluginInstallFunction} install
|
|
1028
|
+
* Function that installs the plugin.
|
|
847
1029
|
* @property {PluginUninstallFunction} [uninstall]
|
|
848
|
-
* Optional function to uninstall the plugin
|
|
1030
|
+
* Optional function to uninstall the plugin.
|
|
849
1031
|
*/ /**
|
|
1032
|
+
* Plugin install function.
|
|
850
1033
|
* @callback PluginInstallFunction
|
|
851
|
-
* @param {Eleva} eleva
|
|
852
|
-
*
|
|
853
|
-
* @
|
|
1034
|
+
* @param {Eleva} eleva
|
|
1035
|
+
* The Eleva instance.
|
|
1036
|
+
* @param {PluginOptions} [options]
|
|
1037
|
+
* Plugin configuration options.
|
|
1038
|
+
* @returns {void | Eleva | unknown}
|
|
854
1039
|
*/ /**
|
|
1040
|
+
* Plugin uninstall function.
|
|
855
1041
|
* @callback PluginUninstallFunction
|
|
856
|
-
* @param {Eleva} eleva
|
|
857
|
-
*
|
|
1042
|
+
* @param {Eleva} eleva
|
|
1043
|
+
* The Eleva instance.
|
|
1044
|
+
* @returns {void | Promise<void>}
|
|
858
1045
|
*/ /**
|
|
1046
|
+
* Configuration options passed to a plugin during installation.
|
|
859
1047
|
* @typedef {Record<string, unknown>} PluginOptions
|
|
860
|
-
* Configuration options passed to a plugin during installation
|
|
861
1048
|
*/ // -----------------------------------------------------------------------------
|
|
862
1049
|
// Event Types
|
|
863
1050
|
// -----------------------------------------------------------------------------
|
|
864
1051
|
/**
|
|
865
|
-
*
|
|
866
|
-
* @
|
|
867
|
-
* @returns {void}
|
|
1052
|
+
* Handler function for DOM events (e.g., click, input, submit).
|
|
1053
|
+
* @typedef {(event: Event) => void} DOMEventHandler
|
|
868
1054
|
*/ /**
|
|
1055
|
+
* Common DOM event names (prefixed with @ in templates).
|
|
869
1056
|
* @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
|
|
870
|
-
* Common DOM event names (prefixed with @ in templates)
|
|
871
1057
|
*/ /**
|
|
872
1058
|
* @class 🧩 Eleva
|
|
873
1059
|
* @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
|
|
874
|
-
*
|
|
1060
|
+
* component styles, and plugin support. Eleva manages component registration, plugin integration,
|
|
875
1061
|
* event handling, and DOM rendering with a focus on performance and developer experience.
|
|
876
1062
|
*
|
|
877
1063
|
* @example
|
|
@@ -901,14 +1087,16 @@
|
|
|
901
1087
|
* The plugin's install function will be called with the Eleva instance and provided options.
|
|
902
1088
|
* After installation, the plugin will be available for use by components.
|
|
903
1089
|
*
|
|
904
|
-
*
|
|
1090
|
+
* @note Plugins that wrap core methods (e.g., mount) must be uninstalled in reverse order
|
|
905
1091
|
* of installation (LIFO - Last In, First Out) to avoid conflicts.
|
|
906
1092
|
*
|
|
907
1093
|
* @public
|
|
908
1094
|
* @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
|
|
909
|
-
* @param {
|
|
910
|
-
* @returns {Eleva} The Eleva instance (for method chaining).
|
|
1095
|
+
* @param {PluginOptions} [options={}] - Optional configuration options for the plugin.
|
|
1096
|
+
* @returns {Eleva | unknown} The Eleva instance (for method chaining) or the result returned by the plugin.
|
|
911
1097
|
* @throws {Error} If plugin does not have an install function.
|
|
1098
|
+
* @see component - Register components after installing plugins.
|
|
1099
|
+
* @see mount - Mount components to the DOM.
|
|
912
1100
|
* @example
|
|
913
1101
|
* app.use(myPlugin, { option1: "value1" });
|
|
914
1102
|
*
|
|
@@ -936,6 +1124,7 @@
|
|
|
936
1124
|
* @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
|
|
937
1125
|
* @returns {Eleva} The Eleva instance (for method chaining).
|
|
938
1126
|
* @throws {Error} If name is not a non-empty string or definition has no template.
|
|
1127
|
+
* @see mount - Mount this component to the DOM.
|
|
939
1128
|
* @example
|
|
940
1129
|
* app.component("myButton", {
|
|
941
1130
|
* template: (ctx) => `<button>${ctx.props.text}</button>`,
|
|
@@ -954,21 +1143,25 @@
|
|
|
954
1143
|
/**
|
|
955
1144
|
* Mounts a registered component to a DOM element.
|
|
956
1145
|
* This will initialize the component, set up its reactive state, and render it to the DOM.
|
|
1146
|
+
* If the container already has a mounted Eleva instance, it is returned as-is.
|
|
1147
|
+
* Unmount clears the container contents and removes the internal instance marker.
|
|
957
1148
|
*
|
|
958
1149
|
* @public
|
|
1150
|
+
* @async
|
|
959
1151
|
* @param {HTMLElement} container - The DOM element where the component will be mounted.
|
|
960
|
-
* @param {string|ComponentDefinition} compName - The name of the registered component or a direct component definition.
|
|
961
|
-
* @param {
|
|
1152
|
+
* @param {string | ComponentDefinition} compName - The name of the registered component or a direct component definition.
|
|
1153
|
+
* @param {ComponentProps} [props={}] - Optional properties to pass to the component.
|
|
962
1154
|
* @returns {Promise<MountResult>}
|
|
963
1155
|
* A Promise that resolves to an object containing:
|
|
964
1156
|
* - container: The mounted component's container element
|
|
965
1157
|
* - data: The component's reactive state and context
|
|
966
1158
|
* - unmount: Function to clean up and unmount the component
|
|
967
1159
|
* @throws {Error} If container is not a DOM element or component is not registered.
|
|
1160
|
+
* @throws {Error} If setup function, template function, or style function throws.
|
|
968
1161
|
* @example
|
|
969
1162
|
* const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
|
|
970
1163
|
* // Later...
|
|
971
|
-
* instance.unmount();
|
|
1164
|
+
* await instance.unmount();
|
|
972
1165
|
*/ async mount(container, compName, props = {}) {
|
|
973
1166
|
if (!container?.nodeType) {
|
|
974
1167
|
throw new Error("Eleva: container must be a DOM element");
|
|
@@ -981,13 +1174,13 @@
|
|
|
981
1174
|
* Destructure the component definition to access core functionality.
|
|
982
1175
|
* - setup: Optional function for component initialization and state management
|
|
983
1176
|
* - template: Required function or string that returns the component's HTML structure
|
|
984
|
-
* - style: Optional function or string for component
|
|
1177
|
+
* - style: Optional function or string for component CSS styles (not auto-scoped)
|
|
985
1178
|
* - children: Optional object defining nested child components
|
|
986
1179
|
*/ const { setup, template, style, children } = definition;
|
|
987
1180
|
/** @type {ComponentContext} */ const context = {
|
|
988
1181
|
props,
|
|
989
1182
|
emitter: this.emitter,
|
|
990
|
-
/** @type {
|
|
1183
|
+
/** @type {SignalFactory} */ signal: (v)=>new this.signal(v)
|
|
991
1184
|
};
|
|
992
1185
|
/**
|
|
993
1186
|
* Processes the mounting of the component.
|
|
@@ -997,19 +1190,20 @@
|
|
|
997
1190
|
* 3. Rendering the component
|
|
998
1191
|
* 4. Managing component lifecycle
|
|
999
1192
|
*
|
|
1000
|
-
* @
|
|
1193
|
+
* @inner
|
|
1194
|
+
* @param {Record<string, unknown>} data - Data returned from the component's setup function.
|
|
1001
1195
|
* @returns {Promise<MountResult>} An object containing:
|
|
1002
1196
|
* - container: The mounted component's container element
|
|
1003
1197
|
* - data: The component's reactive state and context
|
|
1004
1198
|
* - unmount: Function to clean up and unmount the component
|
|
1005
1199
|
*/ const processMount = async (data)=>{
|
|
1006
|
-
/** @type {ComponentContext} */ const mergedContext = {
|
|
1200
|
+
/** @type {ComponentContext & SetupResult} */ const mergedContext = {
|
|
1007
1201
|
...context,
|
|
1008
1202
|
...data
|
|
1009
1203
|
};
|
|
1010
|
-
/** @type {
|
|
1011
|
-
/** @type {
|
|
1012
|
-
/** @type {
|
|
1204
|
+
/** @type {UnsubscribeFunction[]} */ const watchers = [];
|
|
1205
|
+
/** @type {MountResult[]} */ const childInstances = [];
|
|
1206
|
+
/** @type {UnsubscribeFunction[]} */ const listeners = [];
|
|
1013
1207
|
/** @private {boolean} Local mounted state for this component instance */ let isMounted = false;
|
|
1014
1208
|
// ========================================================================
|
|
1015
1209
|
// Render Batching
|
|
@@ -1021,7 +1215,10 @@
|
|
|
1021
1215
|
* changes in the same synchronous block will each call this function,
|
|
1022
1216
|
* but only one render will be scheduled via queueMicrotask.
|
|
1023
1217
|
* This separates concerns: signals handle state, components handle scheduling.
|
|
1218
|
+
*
|
|
1219
|
+
* @inner
|
|
1024
1220
|
* @private
|
|
1221
|
+
* @returns {void}
|
|
1025
1222
|
*/ const scheduleRender = ()=>{
|
|
1026
1223
|
if (renderScheduled) return;
|
|
1027
1224
|
renderScheduled = true;
|
|
@@ -1036,6 +1233,10 @@
|
|
|
1036
1233
|
* 2. Processing the template
|
|
1037
1234
|
* 3. Updating the DOM
|
|
1038
1235
|
* 4. Processing events, injecting styles, and mounting child components.
|
|
1236
|
+
*
|
|
1237
|
+
* @inner
|
|
1238
|
+
* @private
|
|
1239
|
+
* @returns {Promise<void>}
|
|
1039
1240
|
*/ const render = async ()=>{
|
|
1040
1241
|
const html = typeof template === "function" ? await template(mergedContext) : template;
|
|
1041
1242
|
// Execute before hooks
|
|
@@ -1051,6 +1252,18 @@
|
|
|
1051
1252
|
});
|
|
1052
1253
|
}
|
|
1053
1254
|
this.renderer.patchDOM(container, html);
|
|
1255
|
+
// Unmount child components whose host elements were removed by patching.
|
|
1256
|
+
const childrenToUnmount = [];
|
|
1257
|
+
for(let i = childInstances.length - 1; i >= 0; i--){
|
|
1258
|
+
const child = childInstances[i];
|
|
1259
|
+
if (!container.contains(child.container)) {
|
|
1260
|
+
childInstances.splice(i, 1);
|
|
1261
|
+
childrenToUnmount.push(child);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (childrenToUnmount.length) {
|
|
1265
|
+
await Promise.allSettled(childrenToUnmount.map((child)=>child.unmount()));
|
|
1266
|
+
}
|
|
1054
1267
|
this._processEvents(container, mergedContext, listeners);
|
|
1055
1268
|
if (style) this._injectStyles(container, compId, style, mergedContext);
|
|
1056
1269
|
if (children) await this._mountComponents(container, children, childInstances, mergedContext);
|
|
@@ -1073,6 +1286,10 @@
|
|
|
1073
1286
|
* When a Signal's value changes, a batched render is scheduled.
|
|
1074
1287
|
* Multiple changes within the same frame are collapsed into one render.
|
|
1075
1288
|
* Stores unsubscribe functions to clean up watchers when component unmounts.
|
|
1289
|
+
*
|
|
1290
|
+
* @note Signal watchers are invoked synchronously when values change.
|
|
1291
|
+
* Render batching is handled at the component level via queueMicrotask,
|
|
1292
|
+
* not at the signal level. This preserves stack traces for debugging.
|
|
1076
1293
|
*/ for (const val of Object.values(data)){
|
|
1077
1294
|
if (val instanceof Signal) watchers.push(val.watch(scheduleRender));
|
|
1078
1295
|
}
|
|
@@ -1082,15 +1299,16 @@
|
|
|
1082
1299
|
data: mergedContext,
|
|
1083
1300
|
/**
|
|
1084
1301
|
* Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
|
|
1302
|
+
* Removes the internal instance marker from the container when complete.
|
|
1085
1303
|
*
|
|
1086
|
-
* @returns {void}
|
|
1304
|
+
* @returns {Promise<void>}
|
|
1087
1305
|
*/ unmount: async ()=>{
|
|
1088
|
-
|
|
1306
|
+
await mergedContext.onUnmount?.({
|
|
1089
1307
|
container,
|
|
1090
1308
|
context: mergedContext,
|
|
1091
1309
|
cleanup: {
|
|
1092
|
-
watchers
|
|
1093
|
-
listeners
|
|
1310
|
+
watchers,
|
|
1311
|
+
listeners,
|
|
1094
1312
|
children: childInstances
|
|
1095
1313
|
}
|
|
1096
1314
|
});
|
|
@@ -1110,13 +1328,19 @@
|
|
|
1110
1328
|
}
|
|
1111
1329
|
/**
|
|
1112
1330
|
* Processes DOM elements for event binding based on attributes starting with "@".
|
|
1113
|
-
* This method
|
|
1331
|
+
* This method attaches event listeners directly to elements and ensures proper cleanup.
|
|
1332
|
+
* Bound `@event` attributes are removed after listeners are attached.
|
|
1333
|
+
*
|
|
1334
|
+
* Handler resolution order:
|
|
1335
|
+
* 1. Direct context property lookup (e.g., context["handleClick"])
|
|
1336
|
+
* 2. Template expression evaluation via TemplateEngine (e.g., "increment()")
|
|
1114
1337
|
*
|
|
1115
1338
|
* @private
|
|
1116
1339
|
* @param {HTMLElement} container - The container element in which to search for event attributes.
|
|
1117
|
-
* @param {ComponentContext} context - The
|
|
1118
|
-
* @param {
|
|
1340
|
+
* @param {ComponentContext & SetupResult} context - The merged component context and setup data.
|
|
1341
|
+
* @param {UnsubscribeFunction[]} listeners - Array to collect cleanup functions for each event listener.
|
|
1119
1342
|
* @returns {void}
|
|
1343
|
+
* @see TemplateEngine.evaluate - Expression evaluation. fallback.
|
|
1120
1344
|
*/ _processEvents(container, context, listeners) {
|
|
1121
1345
|
/** @type {NodeListOf<Element>} */ const elements = container.querySelectorAll("*");
|
|
1122
1346
|
for (const el of elements){
|
|
@@ -1126,7 +1350,7 @@
|
|
|
1126
1350
|
if (!attr.name.startsWith("@")) continue;
|
|
1127
1351
|
/** @type {keyof HTMLElementEventMap} */ const event = attr.name.slice(1);
|
|
1128
1352
|
/** @type {string} */ const handlerName = attr.value;
|
|
1129
|
-
/** @type {
|
|
1353
|
+
/** @type {DOMEventHandler} */ const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
|
|
1130
1354
|
if (typeof handler === "function") {
|
|
1131
1355
|
el.addEventListener(event, handler);
|
|
1132
1356
|
el.removeAttribute(attr.name);
|
|
@@ -1136,18 +1360,22 @@
|
|
|
1136
1360
|
}
|
|
1137
1361
|
}
|
|
1138
1362
|
/**
|
|
1139
|
-
* Injects
|
|
1140
|
-
*
|
|
1363
|
+
* Injects styles into the component's container.
|
|
1364
|
+
* Styles are placed in a `<style>` element with a `data-e-style` attribute for identification.
|
|
1365
|
+
*
|
|
1366
|
+
* @note Styles are not automatically scoped - use unique class names or CSS nesting for isolation.
|
|
1367
|
+
*
|
|
1368
|
+
* Optimization: Skips DOM update if style content hasn't changed.
|
|
1141
1369
|
*
|
|
1142
1370
|
* @private
|
|
1143
1371
|
* @param {HTMLElement} container - The container element where styles should be injected.
|
|
1144
1372
|
* @param {string} compId - The component ID used to identify the style element.
|
|
1145
|
-
* @param {
|
|
1146
|
-
* @param {ComponentContext} context - The
|
|
1373
|
+
* @param {StyleFunction | string} styleDef - The component's style definition (function or string).
|
|
1374
|
+
* @param {ComponentContext & SetupResult} context - The merged component context and setup data.
|
|
1147
1375
|
* @returns {void}
|
|
1148
1376
|
*/ _injectStyles(container, compId, styleDef, context) {
|
|
1149
1377
|
/** @type {string} */ const newStyle = typeof styleDef === "function" ? styleDef(context) : styleDef;
|
|
1150
|
-
/** @type {HTMLStyleElement|null} */ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
|
|
1378
|
+
/** @type {HTMLStyleElement | null} */ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
|
|
1151
1379
|
if (styleEl && styleEl.textContent === newStyle) return;
|
|
1152
1380
|
if (!styleEl) {
|
|
1153
1381
|
styleEl = document.createElement("style");
|
|
@@ -1160,11 +1388,13 @@
|
|
|
1160
1388
|
* Extracts and evaluates props from an element's attributes that start with `:`.
|
|
1161
1389
|
* Prop values are evaluated as expressions against the component context,
|
|
1162
1390
|
* allowing direct passing of objects, arrays, and other complex types.
|
|
1391
|
+
* Processed attributes are removed from the element after extraction.
|
|
1163
1392
|
*
|
|
1164
1393
|
* @private
|
|
1165
|
-
* @param {HTMLElement} element - The DOM element to extract props from
|
|
1166
|
-
* @param {ComponentContext} context - The component context
|
|
1167
|
-
* @returns {
|
|
1394
|
+
* @param {HTMLElement} element - The DOM element to extract props from.
|
|
1395
|
+
* @param {ComponentContext & SetupResult} context - The merged component context and setup data.
|
|
1396
|
+
* @returns {ComponentProps} An object containing the evaluated props.
|
|
1397
|
+
* @see TemplateEngine.evaluate - Expression evaluation.
|
|
1168
1398
|
* @example
|
|
1169
1399
|
* // For an element with attributes:
|
|
1170
1400
|
* // <div :name="user.name" :data="items">
|
|
@@ -1189,20 +1419,21 @@
|
|
|
1189
1419
|
* This method handles mounting of explicitly defined children components.
|
|
1190
1420
|
*
|
|
1191
1421
|
* The mounting process follows these steps:
|
|
1192
|
-
* 1.
|
|
1422
|
+
* 1. Finds matching DOM nodes within the container
|
|
1193
1423
|
* 2. Mounts explicitly defined children components
|
|
1194
1424
|
*
|
|
1195
1425
|
* @private
|
|
1196
|
-
* @
|
|
1197
|
-
* @param {
|
|
1198
|
-
* @param {
|
|
1199
|
-
* @param {
|
|
1426
|
+
* @async
|
|
1427
|
+
* @param {HTMLElement} container - The container element to mount components in.
|
|
1428
|
+
* @param {ChildrenMap} children - Map of selectors to component definitions for explicit children.
|
|
1429
|
+
* @param {MountResult[]} childInstances - Array to store all mounted component instances.
|
|
1430
|
+
* @param {ComponentContext & SetupResult} context - The merged component context and setup data.
|
|
1200
1431
|
* @returns {Promise<void>}
|
|
1201
1432
|
*
|
|
1202
1433
|
* @example
|
|
1203
1434
|
* // Explicit children mounting:
|
|
1204
1435
|
* const children = {
|
|
1205
|
-
* '
|
|
1436
|
+
* 'user-profile': UserProfileComponent,
|
|
1206
1437
|
* '#settings-panel': "settings-panel"
|
|
1207
1438
|
* };
|
|
1208
1439
|
*/ async _mountComponents(container, children, childInstances, context) {
|
|
@@ -1210,7 +1441,7 @@
|
|
|
1210
1441
|
if (!selector) continue;
|
|
1211
1442
|
for (const el of container.querySelectorAll(selector)){
|
|
1212
1443
|
if (!(el instanceof HTMLElement)) continue;
|
|
1213
|
-
/** @type {
|
|
1444
|
+
/** @type {ComponentProps} */ const props = this._extractProps(el, context);
|
|
1214
1445
|
/** @type {MountResult} */ const instance = await this.mount(el, component, props);
|
|
1215
1446
|
if (instance && !childInstances.includes(instance)) {
|
|
1216
1447
|
childInstances.push(instance);
|
|
@@ -1222,11 +1453,10 @@
|
|
|
1222
1453
|
* Creates a new Eleva instance with the specified name and configuration.
|
|
1223
1454
|
*
|
|
1224
1455
|
* @public
|
|
1456
|
+
* @constructor
|
|
1225
1457
|
* @param {string} name - The unique identifier name for this Eleva instance.
|
|
1226
|
-
* @param {
|
|
1227
|
-
* May include framework-wide settings and default behaviors.
|
|
1458
|
+
* @param {ElevaConfig} [config={}] - Optional configuration object for the instance.
|
|
1228
1459
|
* @throws {Error} If the name is not provided or is not a string.
|
|
1229
|
-
* @returns {Eleva} A new Eleva instance.
|
|
1230
1460
|
*
|
|
1231
1461
|
* @example
|
|
1232
1462
|
* const app = new Eleva("myApp");
|
|
@@ -1240,17 +1470,17 @@
|
|
|
1240
1470
|
if (!name || typeof name !== "string") {
|
|
1241
1471
|
throw new Error("Eleva: name must be a non-empty string");
|
|
1242
1472
|
}
|
|
1243
|
-
/** @public {string} The unique identifier name for this Eleva instance */ this.name = name;
|
|
1244
|
-
/** @public {
|
|
1245
|
-
/** @public {Emitter}
|
|
1246
|
-
/** @public {typeof Signal}
|
|
1247
|
-
/** @public {typeof TemplateEngine}
|
|
1248
|
-
/** @public {Renderer}
|
|
1473
|
+
/** @public @readonly {string} The unique identifier name for this Eleva instance */ this.name = name;
|
|
1474
|
+
/** @public @readonly {Record<string, unknown>} Configuration object for the Eleva instance */ this.config = config;
|
|
1475
|
+
/** @public @readonly {Emitter} Event emitter for handling component events */ this.emitter = new Emitter();
|
|
1476
|
+
/** @public @readonly {typeof Signal} Signal class for creating reactive state */ this.signal = Signal;
|
|
1477
|
+
/** @public @readonly {typeof TemplateEngine} TemplateEngine class for template parsing */ this.templateEngine = TemplateEngine;
|
|
1478
|
+
/** @public @readonly {Renderer} Renderer for handling DOM updates and patching */ this.renderer = new Renderer();
|
|
1249
1479
|
/** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */ this._components = new Map();
|
|
1250
1480
|
/** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */ this._plugins = new Map();
|
|
1251
1481
|
/** @private {number} Counter for generating unique component IDs */ this._componentCounter = 0;
|
|
1252
1482
|
}
|
|
1253
1483
|
}
|
|
1254
1484
|
|
|
1255
|
-
export { Eleva as default };
|
|
1256
|
-
//# sourceMappingURL=eleva.
|
|
1485
|
+
export { Eleva, Emitter, Renderer, Signal, TemplateEngine, Eleva as default };
|
|
1486
|
+
//# sourceMappingURL=eleva.js.map
|