eleva 1.0.0-rc.4 → 1.0.0-rc.6

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.
@@ -0,0 +1,590 @@
1
+ "use strict";
2
+
3
+ import { TemplateEngine } from "../modules/TemplateEngine.js";
4
+
5
+ /**
6
+ * @class 🎯 PropsPlugin
7
+ * @classdesc A plugin that extends Eleva's props data handling to support any type of data structure
8
+ * with automatic type detection, parsing, and reactive prop updates. This plugin enables seamless
9
+ * passing of complex data types from parent to child components without manual parsing.
10
+ *
11
+ * Core Features:
12
+ * - Automatic type detection and parsing (strings, numbers, booleans, objects, arrays, dates, etc.)
13
+ * - Support for complex data structures including nested objects and arrays
14
+ * - Reactive props that automatically update when parent data changes
15
+ * - Comprehensive error handling with custom error callbacks
16
+ * - Simple configuration with minimal setup required
17
+ *
18
+ * @example
19
+ * // Install the plugin
20
+ * const app = new Eleva("myApp");
21
+ * app.use(PropsPlugin, {
22
+ * enableAutoParsing: true,
23
+ * enableReactivity: true,
24
+ * onError: (error, value) => {
25
+ * console.error('Props parsing error:', error, value);
26
+ * }
27
+ * });
28
+ *
29
+ * // Use complex props in components
30
+ * app.component("UserCard", {
31
+ * template: (ctx) => `
32
+ * <div class="user-info-container"
33
+ * :user='${JSON.stringify(ctx.user.value)}'
34
+ * :permissions='${JSON.stringify(ctx.permissions.value)}'
35
+ * :settings='${JSON.stringify(ctx.settings.value)}'>
36
+ * </div>
37
+ * `,
38
+ * children: {
39
+ * '.user-info-container': 'UserInfo'
40
+ * }
41
+ * });
42
+ *
43
+ * app.component("UserInfo", {
44
+ * setup({ props }) {
45
+ * return {
46
+ * user: props.user, // Automatically parsed object
47
+ * permissions: props.permissions, // Automatically parsed array
48
+ * settings: props.settings // Automatically parsed object
49
+ * };
50
+ * }
51
+ * });
52
+ */
53
+ export const PropsPlugin = {
54
+ /**
55
+ * Unique identifier for the plugin
56
+ * @type {string}
57
+ */
58
+ name: "props",
59
+
60
+ /**
61
+ * Plugin version
62
+ * @type {string}
63
+ */
64
+ version: "1.0.0-rc.2",
65
+
66
+ /**
67
+ * Plugin description
68
+ * @type {string}
69
+ */
70
+ description:
71
+ "Advanced props data handling for complex data structures with automatic type detection and reactivity",
72
+
73
+ /**
74
+ * Installs the plugin into the Eleva instance
75
+ *
76
+ * @param {Object} eleva - The Eleva instance
77
+ * @param {Object} options - Plugin configuration options
78
+ * @param {boolean} [options.enableAutoParsing=true] - Enable automatic type detection and parsing
79
+ * @param {boolean} [options.enableReactivity=true] - Enable reactive prop updates using Eleva's signal system
80
+ * @param {Function} [options.onError=null] - Error handler function called when parsing fails
81
+ *
82
+ * @example
83
+ * // Basic installation
84
+ * app.use(PropsPlugin);
85
+ *
86
+ * // Installation with custom options
87
+ * app.use(PropsPlugin, {
88
+ * enableAutoParsing: true,
89
+ * enableReactivity: false,
90
+ * onError: (error, value) => {
91
+ * console.error('Props parsing error:', error, value);
92
+ * }
93
+ * });
94
+ */
95
+ install(eleva, options = {}) {
96
+ const {
97
+ enableAutoParsing = true,
98
+ enableReactivity = true,
99
+ onError = null,
100
+ } = options;
101
+
102
+ /**
103
+ * Detects the type of a given value
104
+ * @private
105
+ * @param {any} value - The value to detect type for
106
+ * @returns {string} The detected type ('string', 'number', 'boolean', 'object', 'array', 'date', 'map', 'set', 'function', 'null', 'undefined', 'unknown')
107
+ *
108
+ * @example
109
+ * detectType("hello") // → "string"
110
+ * detectType(42) // → "number"
111
+ * detectType(true) // → "boolean"
112
+ * detectType([1, 2, 3]) // → "array"
113
+ * detectType({}) // → "object"
114
+ * detectType(new Date()) // → "date"
115
+ * detectType(null) // → "null"
116
+ */
117
+ const detectType = (value) => {
118
+ if (value === null) return "null";
119
+ if (value === undefined) return "undefined";
120
+ if (typeof value === "boolean") return "boolean";
121
+ if (typeof value === "number") return "number";
122
+ if (typeof value === "string") return "string";
123
+ if (typeof value === "function") return "function";
124
+ if (value instanceof Date) return "date";
125
+ if (value instanceof Map) return "map";
126
+ if (value instanceof Set) return "set";
127
+ if (Array.isArray(value)) return "array";
128
+ if (typeof value === "object") return "object";
129
+ return "unknown";
130
+ };
131
+
132
+ /**
133
+ * Parses a prop value with automatic type detection
134
+ * @private
135
+ * @param {any} value - The value to parse
136
+ * @returns {any} The parsed value with appropriate type
137
+ *
138
+ * @description
139
+ * This function automatically detects and parses different data types from string values:
140
+ * - Special strings: "true" → true, "false" → false, "null" → null, "undefined" → undefined
141
+ * - JSON objects/arrays: '{"key": "value"}' → {key: "value"}, '[1, 2, 3]' → [1, 2, 3]
142
+ * - Boolean-like strings: "1" → true, "0" → false, "" → true
143
+ * - Numeric strings: "42" → 42, "3.14" → 3.14
144
+ * - Date strings: "2023-01-01T00:00:00.000Z" → Date object
145
+ * - Other strings: returned as-is
146
+ *
147
+ * @example
148
+ * parsePropValue("true") // → true
149
+ * parsePropValue("42") // → 42
150
+ * parsePropValue('{"key": "val"}') // → {key: "val"}
151
+ * parsePropValue('[1, 2, 3]') // → [1, 2, 3]
152
+ * parsePropValue("hello") // → "hello"
153
+ */
154
+ const parsePropValue = (value) => {
155
+ try {
156
+ // Handle non-string values - return as-is
157
+ if (typeof value !== "string") {
158
+ return value;
159
+ }
160
+
161
+ // Handle special string patterns first
162
+ if (value === "true") return true;
163
+ if (value === "false") return false;
164
+ if (value === "null") return null;
165
+ if (value === "undefined") return undefined;
166
+
167
+ // Try to parse as JSON (for objects and arrays)
168
+ // This handles complex data structures like objects and arrays
169
+ if (value.startsWith("{") || value.startsWith("[")) {
170
+ try {
171
+ return JSON.parse(value);
172
+ } catch (e) {
173
+ // Not valid JSON, throw error to trigger error handler
174
+ throw new Error(`Invalid JSON: ${value}`);
175
+ }
176
+ }
177
+
178
+ // Handle boolean-like strings (including "1" and "0")
179
+ // These are common in HTML attributes and should be treated as booleans
180
+ if (value === "1") return true;
181
+ if (value === "0") return false;
182
+ if (value === "") return true; // Empty string is truthy in HTML attributes
183
+
184
+ // Handle numeric strings (after boolean check to avoid conflicts)
185
+ // This ensures "0" is treated as boolean false, not number 0
186
+ if (!isNaN(value) && value !== "" && !isNaN(parseFloat(value))) {
187
+ return Number(value);
188
+ }
189
+
190
+ // Handle date strings (ISO format)
191
+ // Recognizes standard ISO date format and converts to Date object
192
+ if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
193
+ const date = new Date(value);
194
+ if (!isNaN(date.getTime())) {
195
+ return date;
196
+ }
197
+ }
198
+
199
+ // Return as string if no other parsing applies
200
+ // This is the fallback for regular text strings
201
+ return value;
202
+ } catch (error) {
203
+ // Call error handler if provided
204
+ if (onError) {
205
+ onError(error, value);
206
+ }
207
+ // Fallback to original value to prevent breaking the application
208
+ return value;
209
+ }
210
+ };
211
+
212
+ /**
213
+ * Enhanced props extraction with automatic type detection
214
+ * @private
215
+ * @param {HTMLElement} element - The DOM element to extract props from
216
+ * @returns {Object} Object containing parsed props with appropriate types
217
+ *
218
+ * @description
219
+ * Extracts props from DOM element attributes that start with ":" and automatically
220
+ * parses them to their appropriate types. Removes the attributes from the element
221
+ * after extraction.
222
+ *
223
+ * @example
224
+ * // HTML: <div :name="John" :age="30" :active="true" :data='{"key": "value"}'></div>
225
+ * const props = extractProps(element);
226
+ * // Result: { name: "John", age: 30, active: true, data: {key: "value"} }
227
+ */
228
+ const extractProps = (element) => {
229
+ const props = {};
230
+ const attrs = element.attributes;
231
+
232
+ // Iterate through attributes in reverse order to handle removal correctly
233
+ for (let i = attrs.length - 1; i >= 0; i--) {
234
+ const attr = attrs[i];
235
+ // Only process attributes that start with ":" (prop attributes)
236
+ if (attr.name.startsWith(":")) {
237
+ const propName = attr.name.slice(1); // Remove the ":" prefix
238
+ // Parse the value if auto-parsing is enabled, otherwise use as-is
239
+ const parsedValue = enableAutoParsing
240
+ ? parsePropValue(attr.value)
241
+ : attr.value;
242
+ props[propName] = parsedValue;
243
+ // Remove the attribute from the DOM element after extraction
244
+ element.removeAttribute(attr.name);
245
+ }
246
+ }
247
+
248
+ return props;
249
+ };
250
+
251
+ /**
252
+ * Creates reactive props using Eleva's signal system
253
+ * @private
254
+ * @param {Object} props - The props object to make reactive
255
+ * @returns {Object} Object containing reactive props (Eleva signals)
256
+ *
257
+ * @description
258
+ * Converts regular prop values into Eleva signals for reactive updates.
259
+ * If a value is already a signal, it's passed through unchanged.
260
+ *
261
+ * @example
262
+ * const props = { name: "John", age: 30, active: true };
263
+ * const reactiveProps = createReactiveProps(props);
264
+ * // Result: {
265
+ * // name: Signal("John"),
266
+ * // age: Signal(30),
267
+ * // active: Signal(true)
268
+ * // }
269
+ */
270
+ const createReactiveProps = (props) => {
271
+ const reactiveProps = {};
272
+
273
+ // Convert each prop value to a reactive signal
274
+ Object.entries(props).forEach(([key, value]) => {
275
+ // Check if value is already a signal (has 'value' and 'watch' properties)
276
+ if (
277
+ value &&
278
+ typeof value === "object" &&
279
+ "value" in value &&
280
+ "watch" in value
281
+ ) {
282
+ // Value is already a signal, use it as-is
283
+ reactiveProps[key] = value;
284
+ } else {
285
+ // Create new signal for the prop value to make it reactive
286
+ reactiveProps[key] = new eleva.signal(value);
287
+ }
288
+ });
289
+
290
+ return reactiveProps;
291
+ };
292
+
293
+ // Override Eleva's internal _extractProps method with our enhanced version
294
+ eleva._extractProps = extractProps;
295
+
296
+ // Override Eleva's mount method to apply enhanced prop handling
297
+ const originalMount = eleva.mount;
298
+ eleva.mount = async (container, compName, props = {}) => {
299
+ // Create reactive props if reactivity is enabled
300
+ const enhancedProps = enableReactivity
301
+ ? createReactiveProps(props)
302
+ : props;
303
+
304
+ // Call the original mount method with enhanced props
305
+ return await originalMount.call(
306
+ eleva,
307
+ container,
308
+ compName,
309
+ enhancedProps
310
+ );
311
+ };
312
+
313
+ // Override Eleva's _mountComponents method to enable signal reference passing
314
+ const originalMountComponents = eleva._mountComponents;
315
+
316
+ // Cache to store parent contexts by container element
317
+ const parentContextCache = new WeakMap();
318
+ // Store child instances that need signal linking
319
+ const pendingSignalLinks = new Set();
320
+
321
+ eleva._mountComponents = async (container, children, childInstances) => {
322
+ for (const [selector, component] of Object.entries(children)) {
323
+ if (!selector) continue;
324
+ for (const el of container.querySelectorAll(selector)) {
325
+ if (!(el instanceof HTMLElement)) continue;
326
+
327
+ // Extract props from DOM attributes
328
+ const extractedProps = eleva._extractProps(el);
329
+
330
+ // Get parent context to check for signal references
331
+ let enhancedProps = extractedProps;
332
+
333
+ // Try to find parent context by looking up the DOM tree
334
+ let parentContext = parentContextCache.get(container);
335
+ if (!parentContext) {
336
+ let currentElement = container;
337
+ while (currentElement && !parentContext) {
338
+ if (
339
+ currentElement._eleva_instance &&
340
+ currentElement._eleva_instance.data
341
+ ) {
342
+ parentContext = currentElement._eleva_instance.data;
343
+ // Cache the parent context for future use
344
+ parentContextCache.set(container, parentContext);
345
+ break;
346
+ }
347
+ currentElement = currentElement.parentElement;
348
+ }
349
+ }
350
+
351
+ if (enableReactivity && parentContext) {
352
+ const signalProps = {};
353
+
354
+ // Check each extracted prop to see if there's a matching signal in parent context
355
+ Object.keys(extractedProps).forEach((propName) => {
356
+ if (
357
+ parentContext[propName] &&
358
+ parentContext[propName] instanceof eleva.signal
359
+ ) {
360
+ // Found a signal in parent context with the same name as the prop
361
+ // Pass the signal reference instead of creating a new one
362
+ signalProps[propName] = parentContext[propName];
363
+ }
364
+ });
365
+
366
+ // Merge signal props with regular props (signal props take precedence)
367
+ enhancedProps = {
368
+ ...extractedProps,
369
+ ...signalProps,
370
+ };
371
+ }
372
+
373
+ // Create reactive props for non-signal props only
374
+ let finalProps = enhancedProps;
375
+ if (enableReactivity) {
376
+ // Only create reactive props for values that aren't already signals
377
+ const nonSignalProps = {};
378
+ Object.entries(enhancedProps).forEach(([key, value]) => {
379
+ if (
380
+ !(
381
+ value &&
382
+ typeof value === "object" &&
383
+ "value" in value &&
384
+ "watch" in value
385
+ )
386
+ ) {
387
+ // This is not a signal, create a reactive prop for it
388
+ nonSignalProps[key] = value;
389
+ }
390
+ });
391
+
392
+ // Create reactive props only for non-signal values
393
+ const reactiveNonSignalProps = createReactiveProps(nonSignalProps);
394
+
395
+ // Merge signal props with reactive non-signal props
396
+ finalProps = {
397
+ ...reactiveNonSignalProps,
398
+ ...enhancedProps, // Signal props take precedence
399
+ };
400
+ }
401
+
402
+ /** @type {MountResult} */
403
+ const instance = await eleva.mount(el, component, finalProps);
404
+ if (instance && !childInstances.includes(instance)) {
405
+ childInstances.push(instance);
406
+
407
+ // If we have extracted props but no parent context yet, mark for later signal linking
408
+ if (
409
+ enableReactivity &&
410
+ Object.keys(extractedProps).length > 0 &&
411
+ !parentContext
412
+ ) {
413
+ pendingSignalLinks.add({
414
+ instance,
415
+ extractedProps,
416
+ container,
417
+ component,
418
+ });
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ // After mounting all children, try to link signals for pending instances
425
+ if (enableReactivity && pendingSignalLinks.size > 0) {
426
+ for (const pending of pendingSignalLinks) {
427
+ const { instance, extractedProps, container, component } = pending;
428
+
429
+ // Try to find parent context again
430
+ let parentContext = parentContextCache.get(container);
431
+ if (!parentContext) {
432
+ let currentElement = container;
433
+ while (currentElement && !parentContext) {
434
+ if (
435
+ currentElement._eleva_instance &&
436
+ currentElement._eleva_instance.data
437
+ ) {
438
+ parentContext = currentElement._eleva_instance.data;
439
+ parentContextCache.set(container, parentContext);
440
+ break;
441
+ }
442
+ currentElement = currentElement.parentElement;
443
+ }
444
+ }
445
+
446
+ if (parentContext) {
447
+ const signalProps = {};
448
+
449
+ // Check each extracted prop to see if there's a matching signal in parent context
450
+ Object.keys(extractedProps).forEach((propName) => {
451
+ if (
452
+ parentContext[propName] &&
453
+ parentContext[propName] instanceof eleva.signal
454
+ ) {
455
+ signalProps[propName] = parentContext[propName];
456
+ }
457
+ });
458
+
459
+ // Update the child instance's data with signal references
460
+ if (Object.keys(signalProps).length > 0) {
461
+ Object.assign(instance.data, signalProps);
462
+
463
+ // Set up signal watchers for the newly linked signals
464
+ Object.keys(signalProps).forEach((propName) => {
465
+ const signal = signalProps[propName];
466
+ if (signal && typeof signal.watch === "function") {
467
+ signal.watch((newValue) => {
468
+ // Trigger a re-render of the child component when the signal changes
469
+ const childComponent =
470
+ eleva._components.get(component) || component;
471
+ if (childComponent && childComponent.template) {
472
+ const templateResult =
473
+ typeof childComponent.template === "function"
474
+ ? childComponent.template(instance.data)
475
+ : childComponent.template;
476
+ const newHtml = TemplateEngine.parse(
477
+ templateResult,
478
+ instance.data
479
+ );
480
+ eleva.renderer.patchDOM(instance.container, newHtml);
481
+ }
482
+ });
483
+ }
484
+ });
485
+
486
+ // Initial re-render to show the correct signal values
487
+ const childComponent =
488
+ eleva._components.get(component) || component;
489
+ if (childComponent && childComponent.template) {
490
+ const templateResult =
491
+ typeof childComponent.template === "function"
492
+ ? childComponent.template(instance.data)
493
+ : childComponent.template;
494
+ const newHtml = TemplateEngine.parse(
495
+ templateResult,
496
+ instance.data
497
+ );
498
+ eleva.renderer.patchDOM(instance.container, newHtml);
499
+ }
500
+ }
501
+
502
+ // Remove from pending list
503
+ pendingSignalLinks.delete(pending);
504
+ }
505
+ }
506
+ }
507
+ };
508
+
509
+ /**
510
+ * Expose utility methods on the Eleva instance
511
+ * @namespace eleva.props
512
+ */
513
+ eleva.props = {
514
+ /**
515
+ * Parse a single value with automatic type detection
516
+ * @param {any} value - The value to parse
517
+ * @returns {any} The parsed value with appropriate type
518
+ *
519
+ * @example
520
+ * app.props.parse("42") // → 42
521
+ * app.props.parse("true") // → true
522
+ * app.props.parse('{"key": "val"}') // → {key: "val"}
523
+ */
524
+ parse: (value) => {
525
+ // Return value as-is if auto parsing is disabled
526
+ if (!enableAutoParsing) {
527
+ return value;
528
+ }
529
+ // Use our enhanced parsing function
530
+ return parsePropValue(value);
531
+ },
532
+
533
+ /**
534
+ * Detect the type of a value
535
+ * @param {any} value - The value to detect type for
536
+ * @returns {string} The detected type
537
+ *
538
+ * @example
539
+ * app.props.detectType("hello") // → "string"
540
+ * app.props.detectType(42) // → "number"
541
+ * app.props.detectType([1, 2, 3]) // → "array"
542
+ */
543
+ detectType,
544
+ };
545
+
546
+ // Store original methods for uninstall
547
+ eleva._originalExtractProps = eleva._extractProps;
548
+ eleva._originalMount = originalMount;
549
+ eleva._originalMountComponents = originalMountComponents;
550
+ },
551
+
552
+ /**
553
+ * Uninstalls the plugin from the Eleva instance
554
+ *
555
+ * @param {Object} eleva - The Eleva instance
556
+ *
557
+ * @description
558
+ * Restores the original Eleva methods and removes all plugin-specific
559
+ * functionality. This method should be called when the plugin is no
560
+ * longer needed.
561
+ *
562
+ * @example
563
+ * // Uninstall the plugin
564
+ * PropsPlugin.uninstall(app);
565
+ */
566
+ uninstall(eleva) {
567
+ // Restore original _extractProps method
568
+ if (eleva._originalExtractProps) {
569
+ eleva._extractProps = eleva._originalExtractProps;
570
+ delete eleva._originalExtractProps;
571
+ }
572
+
573
+ // Restore original mount method
574
+ if (eleva._originalMount) {
575
+ eleva.mount = eleva._originalMount;
576
+ delete eleva._originalMount;
577
+ }
578
+
579
+ // Restore original _mountComponents method
580
+ if (eleva._originalMountComponents) {
581
+ eleva._mountComponents = eleva._originalMountComponents;
582
+ delete eleva._originalMountComponents;
583
+ }
584
+
585
+ // Remove plugin utility methods
586
+ if (eleva.props) {
587
+ delete eleva.props;
588
+ }
589
+ },
590
+ };
@@ -32,3 +32,4 @@
32
32
  // Export plugins with clean names
33
33
  export { AttrPlugin as Attr } from "./Attr.js";
34
34
  export { RouterPlugin as Router } from "./Router.js";
35
+ export { PropsPlugin as Props } from "./Props.js";
@@ -0,0 +1,48 @@
1
+ export namespace PropsPlugin {
2
+ let name: string;
3
+ let version: string;
4
+ let description: string;
5
+ /**
6
+ * Installs the plugin into the Eleva instance
7
+ *
8
+ * @param {Object} eleva - The Eleva instance
9
+ * @param {Object} options - Plugin configuration options
10
+ * @param {boolean} [options.enableAutoParsing=true] - Enable automatic type detection and parsing
11
+ * @param {boolean} [options.enableReactivity=true] - Enable reactive prop updates using Eleva's signal system
12
+ * @param {Function} [options.onError=null] - Error handler function called when parsing fails
13
+ *
14
+ * @example
15
+ * // Basic installation
16
+ * app.use(PropsPlugin);
17
+ *
18
+ * // Installation with custom options
19
+ * app.use(PropsPlugin, {
20
+ * enableAutoParsing: true,
21
+ * enableReactivity: false,
22
+ * onError: (error, value) => {
23
+ * console.error('Props parsing error:', error, value);
24
+ * }
25
+ * });
26
+ */
27
+ function install(eleva: Object, options?: {
28
+ enableAutoParsing?: boolean | undefined;
29
+ enableReactivity?: boolean | undefined;
30
+ onError?: Function | undefined;
31
+ }): void;
32
+ /**
33
+ * Uninstalls the plugin from the Eleva instance
34
+ *
35
+ * @param {Object} eleva - The Eleva instance
36
+ *
37
+ * @description
38
+ * Restores the original Eleva methods and removes all plugin-specific
39
+ * functionality. This method should be called when the plugin is no
40
+ * longer needed.
41
+ *
42
+ * @example
43
+ * // Uninstall the plugin
44
+ * PropsPlugin.uninstall(app);
45
+ */
46
+ function uninstall(eleva: Object): void;
47
+ }
48
+ //# sourceMappingURL=Props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Props.d.ts","sourceRoot":"","sources":["../../src/plugins/Props.js"],"names":[],"mappings":";cAuDY,MAAM;iBAMN,MAAM;qBAMN,MAAM;IAKhB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,wBAnBW,MAAM,YAEd;QAA0B,iBAAiB;QACjB,gBAAgB;QACf,OAAO;KAElC,QAodF;IAED;;;;;;;;;;;;;OAaG;IACH,0BAXW,MAAM,QAkChB"}
@@ -1,3 +1,4 @@
1
1
  export { AttrPlugin as Attr } from "./Attr.js";
2
2
  export { RouterPlugin as Router } from "./Router.js";
3
+ export { PropsPlugin as Props } from "./Props.js";
3
4
  //# sourceMappingURL=index.d.ts.map