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