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