assign-gingerly 0.0.28 → 0.0.30

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/Infer.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Symbol for smart value assignment
3
+ * When used with element.set[value], it infers and sets the appropriate value property
4
+ */
5
+ export const value = Symbol.for('assign-gingerly:value');
6
+ /**
7
+ * Symbol for smart display assignment
8
+ * When used with element.set[display], it infers and sets the appropriate display property
9
+ */
10
+ export const display = Symbol.for('assign-gingerly:display');
11
+ /**
12
+ * Enhancement class that provides smart value and display property inference
13
+ * Automatically determines the correct property to set based on element type
14
+ */
15
+ export class Infer {
16
+ #weakRef;
17
+ get enhancedElement() {
18
+ return this.#weakRef.deref();
19
+ }
20
+ constructor(enhancedElement) {
21
+ this.#weakRef = new WeakRef(enhancedElement);
22
+ }
23
+ #value;
24
+ get value() {
25
+ return this.#value;
26
+ }
27
+ set value(nv) {
28
+ this.#value = nv;
29
+ const { enhancedElement } = this;
30
+ enhancedElement[inferValueProperty(enhancedElement)] = nv;
31
+ }
32
+ #display;
33
+ get display() {
34
+ return this.#display;
35
+ }
36
+ set display(nv) {
37
+ this.#display = nv;
38
+ const { enhancedElement } = this;
39
+ enhancedElement[inferDisplayProperty(enhancedElement)] = nv;
40
+ }
41
+ /**
42
+ * Get the inferred event type for the element
43
+ * @returns The most appropriate event type for this element
44
+ */
45
+ get eventType() {
46
+ return inferEventType(this.enhancedElement);
47
+ }
48
+ }
49
+ /**
50
+ * Registry item for the Infer enhancement
51
+ * Register this with customElements.enhancementRegistry to enable smart value/display assignment
52
+ */
53
+ export const registryItem = {
54
+ spawn: Infer,
55
+ enhKey: 'infer',
56
+ symlinks: {
57
+ [value]: 'value',
58
+ [display]: 'display'
59
+ }
60
+ };
61
+ /**
62
+ * Infer the most appropriate value property for an element
63
+ * @param element - The element to infer the property for
64
+ * @returns The property name to use for value assignment
65
+ */
66
+ export function inferValueProperty(element) {
67
+ const tagName = element.localName;
68
+ // Input elements - check type attribute
69
+ if (tagName === 'input') {
70
+ const type = element.getAttribute('type')?.toLowerCase();
71
+ if (type === 'checkbox' || type === 'radio') {
72
+ return 'checked';
73
+ }
74
+ return 'value';
75
+ }
76
+ // Form controls with value property
77
+ if (tagName === 'textarea' || tagName === 'select') {
78
+ return 'value';
79
+ }
80
+ // Semantic HTML elements with specific properties
81
+ if (tagName === 'time') {
82
+ return 'dateTime';
83
+ }
84
+ if (tagName === 'data') {
85
+ return 'value';
86
+ }
87
+ if (tagName === 'meter' || tagName === 'progress') {
88
+ return 'value';
89
+ }
90
+ if (tagName === 'output') {
91
+ return 'value';
92
+ }
93
+ // Check for itemprop attribute as a hint
94
+ const itemprop = element.getAttribute('itemprop');
95
+ if (itemprop) {
96
+ return itemprop;
97
+ }
98
+ // Default fallback
99
+ return 'textContent';
100
+ }
101
+ /**
102
+ * Infer the most appropriate display property for an element
103
+ * @param element - The element to infer the property for
104
+ * @returns The property name to use for display assignment
105
+ */
106
+ export function inferDisplayProperty(element) {
107
+ const tagName = element.localName;
108
+ // Form controls display their value
109
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
110
+ return 'value';
111
+ }
112
+ // Time elements display formatted time
113
+ if (tagName === 'time') {
114
+ return 'textContent';
115
+ }
116
+ // Data elements display human-readable content
117
+ if (tagName === 'data') {
118
+ return 'textContent';
119
+ }
120
+ // Progress/meter elements use ARIA for display
121
+ if (tagName === 'meter' || tagName === 'progress') {
122
+ return 'ariaValueText';
123
+ }
124
+ // Default fallback
125
+ return 'textContent';
126
+ }
127
+ /**
128
+ * Infer the most appropriate event type for an element
129
+ * Used when no explicit event type is provided
130
+ * @param element - The element to infer the event type for
131
+ * @returns The event type name like 'input', 'change', 'click', 'submit'
132
+ */
133
+ export function inferEventType(element) {
134
+ const tagName = element.localName;
135
+ // Form controls that support input event
136
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
137
+ return 'input';
138
+ }
139
+ // Form submission
140
+ if (tagName === 'form') {
141
+ return 'submit';
142
+ }
143
+ // Details element
144
+ if (tagName === 'details') {
145
+ return 'toggle';
146
+ }
147
+ // Dialog element
148
+ if (tagName === 'dialog') {
149
+ return 'close';
150
+ }
151
+ // Default fallback for interactive elements
152
+ return 'click';
153
+ }
154
+ export default registryItem;
package/Infer.ts ADDED
@@ -0,0 +1,190 @@
1
+ import type { EnhancementConfig } from "./types/assign-gingerly/types";
2
+
3
+ /**
4
+ * Symbol for smart value assignment
5
+ * When used with element.set[value], it infers and sets the appropriate value property
6
+ */
7
+ export const value = Symbol.for('assign-gingerly:value');
8
+
9
+ /**
10
+ * Symbol for smart display assignment
11
+ * When used with element.set[display], it infers and sets the appropriate display property
12
+ */
13
+ export const display = Symbol.for('assign-gingerly:display');
14
+
15
+ /**
16
+ * Enhancement class that provides smart value and display property inference
17
+ * Automatically determines the correct property to set based on element type
18
+ */
19
+ export class Infer<TValue = any, TDisplay = any> {
20
+ #weakRef: WeakRef<Element>;
21
+
22
+ get enhancedElement(){
23
+ return this.#weakRef.deref()!;
24
+ }
25
+
26
+ constructor(enhancedElement?: Element){
27
+ this.#weakRef = new WeakRef(enhancedElement!);
28
+ }
29
+
30
+ #value: TValue | undefined;
31
+
32
+ get value(): TValue | undefined {
33
+ return this.#value;
34
+ }
35
+
36
+ set value(nv: TValue){
37
+ this.#value = nv;
38
+ const {enhancedElement} = this;
39
+ (enhancedElement as any)[inferValueProperty(enhancedElement)] = nv;
40
+ }
41
+
42
+ #display: TDisplay | undefined;
43
+
44
+ get display(): TDisplay | undefined {
45
+ return this.#display;
46
+ }
47
+
48
+ set display(nv: TDisplay){
49
+ this.#display = nv;
50
+ const {enhancedElement} = this;
51
+ (enhancedElement as any)[inferDisplayProperty(enhancedElement)] = nv;
52
+ }
53
+
54
+ /**
55
+ * Get the inferred event type for the element
56
+ * @returns The most appropriate event type for this element
57
+ */
58
+ get eventType(): string {
59
+ return inferEventType(this.enhancedElement);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Registry item for the Infer enhancement
65
+ * Register this with customElements.enhancementRegistry to enable smart value/display assignment
66
+ */
67
+ export const registryItem: EnhancementConfig = {
68
+ spawn: Infer,
69
+ enhKey: 'infer',
70
+ symlinks: {
71
+ [value]: 'value',
72
+ [display]: 'display'
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Infer the most appropriate value property for an element
78
+ * @param element - The element to infer the property for
79
+ * @returns The property name to use for value assignment
80
+ */
81
+ export function inferValueProperty(element: Element): string {
82
+ const tagName = element.localName;
83
+
84
+ // Input elements - check type attribute
85
+ if (tagName === 'input') {
86
+ const type = element.getAttribute('type')?.toLowerCase();
87
+ if (type === 'checkbox' || type === 'radio') {
88
+ return 'checked';
89
+ }
90
+ return 'value';
91
+ }
92
+
93
+ // Form controls with value property
94
+ if (tagName === 'textarea' || tagName === 'select') {
95
+ return 'value';
96
+ }
97
+
98
+ // Semantic HTML elements with specific properties
99
+ if (tagName === 'time') {
100
+ return 'dateTime';
101
+ }
102
+
103
+ if (tagName === 'data') {
104
+ return 'value';
105
+ }
106
+
107
+ if (tagName === 'meter' || tagName === 'progress') {
108
+ return 'value';
109
+ }
110
+
111
+ if (tagName === 'output') {
112
+ return 'value';
113
+ }
114
+
115
+ // Check for itemprop attribute as a hint
116
+ const itemprop = element.getAttribute('itemprop');
117
+ if (itemprop) {
118
+ return itemprop;
119
+ }
120
+
121
+ // Default fallback
122
+ return 'textContent';
123
+ }
124
+
125
+ /**
126
+ * Infer the most appropriate display property for an element
127
+ * @param element - The element to infer the property for
128
+ * @returns The property name to use for display assignment
129
+ */
130
+ export function inferDisplayProperty(element: Element): string {
131
+ const tagName = element.localName;
132
+
133
+ // Form controls display their value
134
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
135
+ return 'value';
136
+ }
137
+
138
+ // Time elements display formatted time
139
+ if (tagName === 'time') {
140
+ return 'textContent';
141
+ }
142
+
143
+ // Data elements display human-readable content
144
+ if (tagName === 'data') {
145
+ return 'textContent';
146
+ }
147
+
148
+ // Progress/meter elements use ARIA for display
149
+ if (tagName === 'meter' || tagName === 'progress') {
150
+ return 'ariaValueText';
151
+ }
152
+
153
+ // Default fallback
154
+ return 'textContent';
155
+ }
156
+
157
+ /**
158
+ * Infer the most appropriate event type for an element
159
+ * Used when no explicit event type is provided
160
+ * @param element - The element to infer the event type for
161
+ * @returns The event type name like 'input', 'change', 'click', 'submit'
162
+ */
163
+ export function inferEventType(element: Element): string {
164
+ const tagName = element.localName;
165
+
166
+ // Form controls that support input event
167
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
168
+ return 'input';
169
+ }
170
+
171
+ // Form submission
172
+ if (tagName === 'form') {
173
+ return 'submit';
174
+ }
175
+
176
+ // Details element
177
+ if (tagName === 'details') {
178
+ return 'toggle';
179
+ }
180
+
181
+ // Dialog element
182
+ if (tagName === 'dialog') {
183
+ return 'close';
184
+ }
185
+
186
+ // Default fallback for interactive elements
187
+ return 'click';
188
+ }
189
+
190
+ export default registryItem;
package/README.md CHANGED
@@ -1427,6 +1427,150 @@ element.enh.dispose(registryItem); // Stops timer and cleans up
1427
1427
  - Calling `enh.get()` again will create a new instance
1428
1428
  - The enhancement property is removed from the enh container
1429
1429
 
1430
+ #### Memory Management and When to Call Dispose
1431
+
1432
+ **Important: Understanding automatic vs manual cleanup**
1433
+
1434
+ The enhancement storage system uses a **WeakMap** to prevent memory leaks:
1435
+
1436
+ ```TypeScript
1437
+ // Global storage: WeakMap<Element, Map<EnhancementConfig, Instance>>
1438
+ ```
1439
+
1440
+ **What this means for memory:**
1441
+
1442
+ ✅ **Automatic cleanup when elements are garbage collected:**
1443
+ - When an element is GC'd, the WeakMap entry is automatically removed
1444
+ - Both `enhKey` references (`element.enh[enhKey]`) and WeakMap entries are cleaned up
1445
+ - **No memory leak from the storage mechanism itself**
1446
+
1447
+ ⚠️ **Manual cleanup needed for enhancement internals:**
1448
+ - Event listeners on global objects (window, document)
1449
+ - Timers (setInterval, setTimeout)
1450
+ - External registries or caches
1451
+ - Network connections or subscriptions
1452
+
1453
+ **The challenge: Knowing WHEN to dispose**
1454
+
1455
+ JavaScript provides no way to detect when an element is about to be garbage collected. Additionally, DOM disconnection doesn't reliably indicate disposal:
1456
+
1457
+ ```TypeScript
1458
+ // Element disconnected - but should we dispose?
1459
+ element.remove();
1460
+
1461
+ // Case 1: Temporarily removed, will be re-added
1462
+ setTimeout(() => document.body.append(element), 1000);
1463
+ // ❌ Don't dispose - enhancement should persist
1464
+
1465
+ // Case 2: Moved to another location
1466
+ otherContainer.append(element);
1467
+ // ❌ Don't dispose - enhancement should persist
1468
+
1469
+ // Case 3: Cached for reuse
1470
+ elementCache.set('myElement', element);
1471
+ // ❌ Don't dispose - enhancement should persist
1472
+
1473
+ // Case 4: Truly done, ready for GC
1474
+ element = null;
1475
+ // ✅ Should dispose, but no way to detect this automatically
1476
+ ```
1477
+
1478
+ **Practical disposal strategies:**
1479
+
1480
+ 1. **Short-lived elements:** Don't worry about disposal - WeakMap handles cleanup automatically when elements are GC'd
1481
+
1482
+ 2. **Long-lived applications:** Implement manual disposal at logical boundaries:
1483
+ ```TypeScript
1484
+ // On route change
1485
+ router.beforeLeave(() => {
1486
+ oldRouteElements.forEach(el => el.enh.dispose(registryItem));
1487
+ });
1488
+
1489
+ // On explicit user action
1490
+ closeButton.onclick = () => {
1491
+ dialog.enh.dispose(registryItem);
1492
+ dialog.remove();
1493
+ };
1494
+ ```
1495
+
1496
+ 3. **Framework integration:** Use framework lifecycle hooks:
1497
+ ```TypeScript
1498
+ // React
1499
+ useEffect(() => {
1500
+ return () => elementRef.current?.enh.dispose(registryItem);
1501
+ }, []);
1502
+
1503
+ // Vue
1504
+ onUnmounted(() => {
1505
+ element.value?.enh.dispose(registryItem);
1506
+ });
1507
+ ```
1508
+
1509
+ 4. **MutationObserver heuristic:** Watch for disconnection + timeout (imperfect but practical):
1510
+ ```TypeScript
1511
+ const observer = new MutationObserver(() => {
1512
+ if (!element.isConnected) {
1513
+ setTimeout(() => {
1514
+ if (!element.isConnected) {
1515
+ element.enh.dispose(registryItem);
1516
+ }
1517
+ }, 5000); // If still disconnected after 5s, probably done
1518
+ }
1519
+ });
1520
+ ```
1521
+
1522
+ **Best practices for enhancement authors:**
1523
+
1524
+ Always implement proper cleanup in your dispose method:
1525
+
1526
+ ```TypeScript
1527
+ class MyEnhancement {
1528
+ element;
1529
+ timerId = null;
1530
+ boundHandler = null;
1531
+
1532
+ constructor(element, ctx) {
1533
+ this.element = element;
1534
+ this.boundHandler = this.handleClick.bind(this);
1535
+
1536
+ // Local listener - OK, will be GC'd with element
1537
+ element.addEventListener('click', this.boundHandler);
1538
+
1539
+ // Global listener - MUST clean up manually
1540
+ window.addEventListener('resize', this.boundHandler);
1541
+
1542
+ // Timer - MUST clean up manually
1543
+ this.timerId = setInterval(() => this.update(), 1000);
1544
+ }
1545
+
1546
+ dispose() {
1547
+ // Clean up global listener
1548
+ if (this.boundHandler) {
1549
+ window.removeEventListener('resize', this.boundHandler);
1550
+ }
1551
+
1552
+ // Clean up timer
1553
+ if (this.timerId) {
1554
+ clearInterval(this.timerId);
1555
+ this.timerId = null;
1556
+ }
1557
+
1558
+ // Clear references
1559
+ this.element = null;
1560
+ this.boundHandler = null;
1561
+ }
1562
+
1563
+ handleClick() { /* ... */ }
1564
+ update() { /* ... */ }
1565
+ }
1566
+ ```
1567
+
1568
+ **Summary:**
1569
+ - ✅ Storage mechanism prevents memory leaks via WeakMap
1570
+ - ⚠️ Enhancement internals need manual cleanup via dispose()
1571
+ - ❌ No automatic way to detect when disposal should happen
1572
+ - 👍 Choose disposal strategy based on your application's lifecycle
1573
+
1430
1574
  ### Waiting for Async Initialization with `enh.whenResolved(regItem)`
1431
1575
 
1432
1576
  The `enh.whenResolved(regItem)` method provides a way to wait for asynchronous enhancement initialization:
@@ -3267,3 +3411,312 @@ This design ensures backward compatibility while providing powerful new capabili
3267
3411
 
3268
3412
 
3269
3413
 
3414
+ ## Smart Value Assignment with Infer Enhancement
3415
+
3416
+ The Infer enhancement provides a symbol-based API for smart value and display property assignment. Instead of manually determining which property to set on different element types (e.g., `value` for inputs, `checked` for checkboxes, `textContent` for divs), the Infer enhancement automatically infers the correct property based on the element type.
3417
+
3418
+ ### Why Infer?
3419
+
3420
+ Different HTML elements use different properties to represent their value:
3421
+ - Input text fields use `value`
3422
+ - Checkboxes and radio buttons use `checked`
3423
+ - Time elements use `dateTime`
3424
+ - Divs and spans use `textContent`
3425
+ - Progress and meter elements use `value` but display with `ariaValueText`
3426
+
3427
+ The Infer enhancement eliminates the need to remember these differences by providing two symbols that automatically map to the correct property:
3428
+
3429
+ - `value` symbol - Sets the element's data value
3430
+ - `display` symbol - Sets the element's display/presentation value
3431
+
3432
+ ### Basic Usage
3433
+
3434
+ ```TypeScript
3435
+ import { value, display, registryItem } from 'assign-gingerly/Infer.js';
3436
+ import 'assign-gingerly/object-extension.js';
3437
+
3438
+ // Register the Infer enhancement
3439
+ customElements.enhancementRegistry.push(registryItem);
3440
+
3441
+ // Use the value symbol - automatically sets the right property
3442
+ const input = document.createElement('input');
3443
+ input.type = 'text';
3444
+ input.set[value] = 'hello';
3445
+ console.log(input.value); // 'hello'
3446
+
3447
+ const checkbox = document.createElement('input');
3448
+ checkbox.type = 'checkbox';
3449
+ checkbox.set[value] = true;
3450
+ console.log(checkbox.checked); // true
3451
+
3452
+ const div = document.createElement('div');
3453
+ div.set[value] = 'content';
3454
+ console.log(div.textContent); // 'content'
3455
+
3456
+ const time = document.createElement('time');
3457
+ time.set[value] = '2024-01-01T00:00:00Z';
3458
+ console.log(time.dateTime); // '2024-01-01T00:00:00Z'
3459
+ ```
3460
+
3461
+ ### Value Property Inference
3462
+
3463
+ The `value` symbol automatically maps to the most appropriate property for each element type:
3464
+
3465
+ | Element Type | Property Set | Example |
3466
+ |-------------|-------------|---------|
3467
+ | `<input type="text">` | `value` | Text input value |
3468
+ | `<input type="checkbox">` | `checked` | Checkbox state |
3469
+ | `<input type="radio">` | `checked` | Radio button state |
3470
+ | `<textarea>` | `value` | Textarea content |
3471
+ | `<select>` | `value` | Selected option |
3472
+ | `<time>` | `dateTime` | ISO datetime string |
3473
+ | `<data>` | `value` | Machine-readable value |
3474
+ | `<meter>` | `value` | Numeric value |
3475
+ | `<progress>` | `value` | Progress value |
3476
+ | `<output>` | `value` | Output value |
3477
+ | Elements with `itemprop` | `itemprop` value | Custom property name |
3478
+ | Other elements | `textContent` | Text content |
3479
+
3480
+ ### Display Property Inference
3481
+
3482
+ The `display` symbol sets the human-readable display value:
3483
+
3484
+ ```TypeScript
3485
+ // Time element - display formatted time
3486
+ const time = document.createElement('time');
3487
+ time.set[value] = '2024-01-01T00:00:00Z'; // Machine-readable
3488
+ time.set[display] = 'January 1, 2024'; // Human-readable
3489
+ console.log(time.dateTime); // '2024-01-01T00:00:00Z'
3490
+ console.log(time.textContent); // 'January 1, 2024'
3491
+
3492
+ // Meter element - display with ARIA
3493
+ const meter = document.createElement('meter');
3494
+ meter.min = 0;
3495
+ meter.max = 100;
3496
+ meter.set[value] = 75; // Numeric value
3497
+ meter.set[display] = '75 percent'; // Screen reader text
3498
+ console.log(meter.value); // 75
3499
+ console.log(meter.ariaValueText); // '75 percent'
3500
+ ```
3501
+
3502
+ | Element Type | Property Set | Example |
3503
+ |-------------|-------------|---------|
3504
+ | `<input>`, `<textarea>`, `<select>` | `value` | Form control value |
3505
+ | `<time>` | `textContent` | Formatted time string |
3506
+ | `<data>` | `textContent` | Human-readable content |
3507
+ | `<meter>`, `<progress>` | `ariaValueText` | Screen reader text |
3508
+ | Other elements | `textContent` | Text content |
3509
+
3510
+ ### Accessing the Enhancement Instance
3511
+
3512
+ The Infer enhancement is accessible via `element.enh.infer`:
3513
+
3514
+ ```TypeScript
3515
+ const input = document.createElement('input');
3516
+ input.set[value] = 'test';
3517
+
3518
+ // Access the enhancement instance
3519
+ console.log(input.enh.infer.value); // 'test' (cached value)
3520
+
3521
+ // The instance maintains references to both value and display
3522
+ input.set[display] = 'Test Display';
3523
+ console.log(input.enh.infer.value); // 'test'
3524
+ console.log(input.enh.infer.display); // 'Test Display'
3525
+ ```
3526
+
3527
+ ### Using with assignGingerly
3528
+
3529
+ The Infer enhancement integrates seamlessly with `assignGingerly`:
3530
+
3531
+ ```TypeScript
3532
+ import { value, display } from 'assign-gingerly/Infer.js';
3533
+
3534
+ const element = document.createElement('input');
3535
+ element.type = 'text';
3536
+
3537
+ // Use symbols in assignGingerly
3538
+ element.assignGingerly({
3539
+ [value]: 'hello world',
3540
+ style: {
3541
+ color: 'blue'
3542
+ }
3543
+ });
3544
+
3545
+ console.log(element.value); // 'hello world'
3546
+ console.log(element.style.color); // 'blue'
3547
+ ```
3548
+
3549
+ ### Itemprop Support
3550
+
3551
+ Elements with an `itemprop` attribute use that attribute's value as the property name:
3552
+
3553
+ ```html
3554
+ <span itemprop="title"></span>
3555
+ ```
3556
+
3557
+ ```TypeScript
3558
+ const span = document.querySelector('[itemprop="title"]');
3559
+ span.set[value] = 'My Title';
3560
+ console.log(span.title); // 'My Title'
3561
+ ```
3562
+
3563
+ ### Implementation Details
3564
+
3565
+ The Infer enhancement is implemented as a standard enhancement class:
3566
+
3567
+ ```TypeScript
3568
+ class Infer<TValue = any, TDisplay = any> {
3569
+ #weakRef: WeakRef<Element>;
3570
+
3571
+ constructor(enhancedElement?: Element) {
3572
+ this.#weakRef = new WeakRef(enhancedElement!);
3573
+ }
3574
+
3575
+ get value(): TValue | undefined { /* ... */ }
3576
+ set value(nv: TValue) {
3577
+ const element = this.#weakRef.deref()!;
3578
+ element[inferValueProperty(element)] = nv;
3579
+ }
3580
+
3581
+ get display(): TDisplay | undefined { /* ... */ }
3582
+ set display(nv: TDisplay) {
3583
+ const element = this.#weakRef.deref()!;
3584
+ element[inferDisplayProperty(element)] = nv;
3585
+ }
3586
+ }
3587
+ ```
3588
+
3589
+ **Registry Configuration:**
3590
+
3591
+ ```TypeScript
3592
+ export const registryItem: EnhancementConfig = {
3593
+ spawn: Infer,
3594
+ enhKey: 'infer',
3595
+ symlinks: {
3596
+ [value]: 'value',
3597
+ [display]: 'display'
3598
+ }
3599
+ };
3600
+ ```
3601
+
3602
+ The `symlinks` mapping connects the symbols to the enhancement's properties, enabling the `element.set[symbol]` syntax.
3603
+
3604
+ ### Helper Functions
3605
+
3606
+ The Infer module exports helper functions for manual property and event type inference:
3607
+
3608
+ ```TypeScript
3609
+ import { inferValueProperty, inferDisplayProperty, inferEventType } from 'assign-gingerly/Infer.js';
3610
+
3611
+ const input = document.createElement('input');
3612
+ input.type = 'checkbox';
3613
+
3614
+ const valueProp = inferValueProperty(input);
3615
+ console.log(valueProp); // 'checked'
3616
+
3617
+ const displayProp = inferDisplayProperty(input);
3618
+ console.log(displayProp); // 'value'
3619
+
3620
+ const eventType = inferEventType(input);
3621
+ console.log(eventType); // 'input'
3622
+ ```
3623
+
3624
+ These functions can be useful when you need to determine the property or event type name without actually setting a value or attaching a listener.
3625
+
3626
+ **Event Type Inference:**
3627
+
3628
+ The `inferEventType` function returns the most appropriate event type for different element types:
3629
+
3630
+ | Element Type | Event Type | Use Case |
3631
+ |-------------|-----------|----------|
3632
+ | `<input>`, `<textarea>`, `<select>` | `input` | Form control value changes |
3633
+ | `<form>` | `submit` | Form submission |
3634
+ | `<details>` | `toggle` | Details element open/close |
3635
+ | `<dialog>` | `close` | Dialog dismissal |
3636
+ | Other elements | `click` | Default interactive event |
3637
+
3638
+ **Accessing via Enhancement Instance:**
3639
+
3640
+ The inferred event type is also available as a getter on the enhancement instance:
3641
+
3642
+ ```TypeScript
3643
+ const input = document.createElement('input');
3644
+ input.set[value] = 'test';
3645
+
3646
+ console.log(input.enh.infer.eventType); // 'input'
3647
+
3648
+ const form = document.createElement('form');
3649
+ form.set[value] = 'test';
3650
+
3651
+ console.log(form.enh.infer.eventType); // 'submit'
3652
+ ```
3653
+
3654
+ This is particularly useful when building enhancements that need to attach event listeners but don't know the element type in advance.
3655
+
3656
+ ### Benefits
3657
+
3658
+ 1. **Type-agnostic code**: Write code that works with any element type without conditionals
3659
+ 2. **Cleaner syntax**: No need to remember which property each element type uses
3660
+ 3. **Accessibility**: Separate value and display properties support screen readers
3661
+ 4. **Framework-friendly**: Symbols work well with reactive frameworks and data binding
3662
+ 5. **Extensible**: Based on the enhancement registry system, can be customized or extended
3663
+
3664
+ ### Complete Example
3665
+
3666
+ ```html
3667
+ <!DOCTYPE html>
3668
+ <html>
3669
+ <head>
3670
+ <script type="module">
3671
+ import { value, display, registryItem } from './Infer.js';
3672
+ import './object-extension.js';
3673
+
3674
+ // Register the enhancement
3675
+ customElements.enhancementRegistry.push(registryItem);
3676
+
3677
+ // Create various elements
3678
+ const input = document.createElement('input');
3679
+ input.type = 'text';
3680
+
3681
+ const checkbox = document.createElement('input');
3682
+ checkbox.type = 'checkbox';
3683
+
3684
+ const time = document.createElement('time');
3685
+
3686
+ const meter = document.createElement('meter');
3687
+ meter.min = 0;
3688
+ meter.max = 100;
3689
+
3690
+ // Set values using the same symbol - each element handles it correctly
3691
+ input.set[value] = 'Hello World';
3692
+ checkbox.set[value] = true;
3693
+ time.set[value] = '2024-01-01T00:00:00Z';
3694
+ time.set[display] = 'January 1, 2024';
3695
+ meter.set[value] = 75;
3696
+ meter.set[display] = '75 percent';
3697
+
3698
+ // Add to document
3699
+ document.body.append(input, checkbox, time, meter);
3700
+
3701
+ console.log('Input value:', input.value); // 'Hello World'
3702
+ console.log('Checkbox checked:', checkbox.checked); // true
3703
+ console.log('Time dateTime:', time.dateTime); // '2024-01-01T00:00:00Z'
3704
+ console.log('Time display:', time.textContent); // 'January 1, 2024'
3705
+ console.log('Meter value:', meter.value); // 75
3706
+ console.log('Meter display:', meter.ariaValueText); // '75 percent'
3707
+ </script>
3708
+ </head>
3709
+ <body>
3710
+ <h1>Infer Enhancement Demo</h1>
3711
+ </body>
3712
+ </html>
3713
+ ```
3714
+
3715
+ ### Browser Support
3716
+
3717
+ The Infer enhancement requires:
3718
+ - Chrome 146+ (for scoped custom element registries)
3719
+ - Modern browsers with Symbol support
3720
+ - WeakRef support (all modern browsers)
3721
+
3722
+ For browsers without scoped registry support, the enhancement falls back to the global `customElements.enhancementRegistry`.
package/getHost.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Get the itemscope host element for a given element.
3
+ * This function finds the closest element with an itemscope attribute and waits for it to be ready.
4
+ *
5
+ * @param el - The element to start searching from
6
+ * @returns The itemscope host element, or null if none found
7
+ */
8
+ export async function getHost(el) {
9
+ const itemScopeHost = el.closest('[itemscope]');
10
+ if (itemScopeHost) {
11
+ const { localName } = itemScopeHost;
12
+ // If it's a custom element, wait for it to be defined
13
+ if (localName.includes('-')) {
14
+ const registry = itemScopeHost.customElementRegistry ?? customElements;
15
+ await registry.whenDefined(localName);
16
+ return itemScopeHost;
17
+ }
18
+ else {
19
+ // Check if itemscope specifies a value (manager name)
20
+ const itemscopeValue = itemScopeHost.getAttribute('itemscope');
21
+ if (itemscopeValue && itemscopeValue.length > 0) {
22
+ // Wait for the manager to be defined in the itemscope registry
23
+ const registry = itemScopeHost.customElementRegistry?.itemscopeRegistry
24
+ ?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
25
+ if (registry) {
26
+ await registry.whenDefined(itemscopeValue);
27
+ }
28
+ }
29
+ return itemScopeHost;
30
+ }
31
+ }
32
+ else {
33
+ // No itemscope host found in the light DOM
34
+ // Check if we're inside a shadow root and get the shadow host
35
+ const rootNode = el.getRootNode();
36
+ // Check if it's a shadow root (has a host property)
37
+ if (rootNode && 'host' in rootNode && rootNode.host) {
38
+ const host = rootNode.host;
39
+ const { localName } = host;
40
+ // If the host is a custom element, wait for it to be defined
41
+ if (localName.includes('-')) {
42
+ const registry = host.customElementRegistry ?? customElements;
43
+ await registry.whenDefined(localName);
44
+ }
45
+ return host;
46
+ }
47
+ // Not in a shadow root either
48
+ return null;
49
+ }
50
+ }
51
+ export default getHost;
package/getHost.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Get the itemscope host element for a given element.
3
+ * This function finds the closest element with an itemscope attribute and waits for it to be ready.
4
+ *
5
+ * @param el - The element to start searching from
6
+ * @returns The itemscope host element, or null if none found
7
+ */
8
+ export async function getHost(el: Element): Promise<Element | null> {
9
+ const itemScopeHost = el.closest('[itemscope]');
10
+ if (itemScopeHost) {
11
+ const { localName } = itemScopeHost;
12
+
13
+ // If it's a custom element, wait for it to be defined
14
+ if (localName.includes('-')) {
15
+ const registry = (itemScopeHost as any).customElementRegistry ?? customElements;
16
+ await registry.whenDefined(localName);
17
+ return itemScopeHost;
18
+ } else {
19
+ // Check if itemscope specifies a value (manager name)
20
+ const itemscopeValue = itemScopeHost.getAttribute('itemscope');
21
+
22
+ if (itemscopeValue && itemscopeValue.length > 0) {
23
+ // Wait for the manager to be defined in the itemscope registry
24
+ const registry = (itemScopeHost as any).customElementRegistry?.itemscopeRegistry
25
+ ?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
26
+
27
+ if (registry) {
28
+ await registry.whenDefined(itemscopeValue);
29
+ }
30
+ }
31
+
32
+ return itemScopeHost;
33
+ }
34
+ } else {
35
+ // No itemscope host found in the light DOM
36
+ // Check if we're inside a shadow root and get the shadow host
37
+ const rootNode = el.getRootNode();
38
+
39
+ // Check if it's a shadow root (has a host property)
40
+ if (rootNode && 'host' in rootNode && (rootNode as ShadowRoot).host) {
41
+ const host = (rootNode as ShadowRoot).host;
42
+ const { localName } = host;
43
+
44
+ // If the host is a custom element, wait for it to be defined
45
+ if (localName.includes('-')) {
46
+ const registry = (host as any).customElementRegistry ?? customElements;
47
+ await registry.whenDefined(localName);
48
+ }
49
+
50
+ return host;
51
+ }
52
+
53
+ // Not in a shadow root either
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export default getHost;
package/index.js CHANGED
@@ -6,4 +6,5 @@ export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
6
6
  export { parseWithAttrs } from './parseWithAttrs.js';
7
7
  export { buildCSSQuery } from './buildCSSQuery.js';
8
8
  export { resolveTemplate } from './resolveTemplate.js';
9
+ export { getHost } from './getHost.js';
9
10
  import './object-extension.js';
package/index.ts CHANGED
@@ -6,4 +6,5 @@ export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
6
6
  export {parseWithAttrs} from './parseWithAttrs.js';
7
7
  export {buildCSSQuery} from './buildCSSQuery.js';
8
8
  export {resolveTemplate} from './resolveTemplate.js';
9
+ export {getHost} from './getHost.js';
9
10
  import './object-extension.js';
@@ -284,6 +284,74 @@ if (typeof Element !== 'undefined') {
284
284
  enumerable: true,
285
285
  configurable: true,
286
286
  });
287
+ /**
288
+ * Adds 'set' property to Element prototype for symbol-based dependency injection
289
+ * Returns a proxy that intercepts symbol property assignments
290
+ */
291
+ Object.defineProperty(Element.prototype, 'set', {
292
+ get: function () {
293
+ const element = this;
294
+ return new Proxy({}, {
295
+ set: (_, prop, value) => {
296
+ if (typeof prop === 'symbol') {
297
+ // Get the registry from customElementRegistry (scoped or global)
298
+ const registry = element.customElementRegistry?.enhancementRegistry
299
+ ?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
300
+ if (registry) {
301
+ const registryItem = registry.findBySymbol(prop);
302
+ if (registryItem) {
303
+ const instanceMap = getInstanceMap();
304
+ const instances = instanceMap.getOrInsertComputed(element, () => new Map());
305
+ let instance = instances.get(registryItem);
306
+ if (!instance) {
307
+ const SpawnClass = registryItem.spawn;
308
+ // Check canSpawn if it exists
309
+ if (typeof SpawnClass.canSpawn === 'function') {
310
+ const ctx = { config: registryItem };
311
+ if (!SpawnClass.canSpawn(element, ctx)) {
312
+ // canSpawn returned false, skip spawning
313
+ return true;
314
+ }
315
+ }
316
+ // If target is an Element and registryItem has enhKey, pass element to constructor
317
+ if (registryItem.enhKey) {
318
+ const ctx = { config: registryItem };
319
+ const initVals = element.enh?.[registryItem.enhKey] &&
320
+ !(element.enh[registryItem.enhKey] instanceof SpawnClass)
321
+ ? element.enh[registryItem.enhKey]
322
+ : undefined;
323
+ instance = new SpawnClass(element, ctx, initVals);
324
+ }
325
+ else {
326
+ const ctx = { config: registryItem };
327
+ instance = new SpawnClass(element, ctx);
328
+ }
329
+ instances.set(registryItem, instance);
330
+ // If registryItem has enhKey, store on enh
331
+ if (registryItem.enhKey) {
332
+ if (!element.enh) {
333
+ // This shouldn't happen since enh is a getter, but be safe
334
+ element.enh = {};
335
+ }
336
+ element.enh[registryItem.enhKey] = instance;
337
+ }
338
+ }
339
+ if (registryItem.symlinks) {
340
+ const mappedKey = registryItem.symlinks[prop];
341
+ if (mappedKey && instance && typeof instance === 'object') {
342
+ instance[mappedKey] = value;
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ return true;
349
+ },
350
+ });
351
+ },
352
+ enumerable: false,
353
+ configurable: true,
354
+ });
287
355
  }
288
356
  /**
289
357
  * Adds assignGingerly method to all objects via the Object prototype
@@ -404,6 +404,84 @@ if (typeof Element !== 'undefined') {
404
404
  enumerable: true,
405
405
  configurable: true,
406
406
  });
407
+
408
+ /**
409
+ * Adds 'set' property to Element prototype for symbol-based dependency injection
410
+ * Returns a proxy that intercepts symbol property assignments
411
+ */
412
+ Object.defineProperty(Element.prototype, 'set', {
413
+ get: function (this: Element) {
414
+ const element = this;
415
+ return new Proxy(
416
+ {},
417
+ {
418
+ set: (_: any, prop: string | symbol, value: any) => {
419
+ if (typeof prop === 'symbol') {
420
+ // Get the registry from customElementRegistry (scoped or global)
421
+ const registry = (element as any).customElementRegistry?.enhancementRegistry
422
+ ?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
423
+
424
+ if (registry) {
425
+ const registryItem = registry.findBySymbol(prop);
426
+ if (registryItem) {
427
+ const instanceMap = getInstanceMap();
428
+ const instances = instanceMap.getOrInsertComputed(element, () => new Map());
429
+ let instance = instances.get(registryItem);
430
+
431
+ if (!instance) {
432
+ const SpawnClass = registryItem.spawn;
433
+
434
+ // Check canSpawn if it exists
435
+ if (typeof SpawnClass.canSpawn === 'function') {
436
+ const ctx = { config: registryItem };
437
+ if (!SpawnClass.canSpawn(element, ctx)) {
438
+ // canSpawn returned false, skip spawning
439
+ return true;
440
+ }
441
+ }
442
+
443
+ // If target is an Element and registryItem has enhKey, pass element to constructor
444
+ if (registryItem.enhKey) {
445
+ const ctx = { config: registryItem };
446
+ const initVals = (element as any).enh?.[registryItem.enhKey] &&
447
+ !((element as any).enh[registryItem.enhKey] instanceof SpawnClass)
448
+ ? (element as any).enh[registryItem.enhKey]
449
+ : undefined;
450
+ instance = new SpawnClass(element, ctx, initVals);
451
+ } else {
452
+ const ctx = { config: registryItem };
453
+ instance = new SpawnClass(element, ctx);
454
+ }
455
+
456
+ instances.set(registryItem, instance);
457
+
458
+ // If registryItem has enhKey, store on enh
459
+ if (registryItem.enhKey) {
460
+ if (!(element as any).enh) {
461
+ // This shouldn't happen since enh is a getter, but be safe
462
+ (element as any).enh = {};
463
+ }
464
+ (element as any).enh[registryItem.enhKey] = instance;
465
+ }
466
+ }
467
+
468
+ if(registryItem.symlinks){
469
+ const mappedKey = registryItem.symlinks[prop];
470
+ if (mappedKey && instance && typeof instance === 'object') {
471
+ (instance as any)[mappedKey] = value;
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+ return true;
478
+ },
479
+ }
480
+ );
481
+ },
482
+ enumerable: false,
483
+ configurable: true,
484
+ });
407
485
  }
408
486
 
409
487
  /**
package/package.json CHANGED
@@ -1,71 +1,79 @@
1
- {
2
- "name": "assign-gingerly",
3
- "version": "0.0.28",
4
- "description": "This package provides a utility function for carefully merging one object into another.",
5
- "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
- "bugs": {
7
- "url": "https://github.com/bahrus/assign-gingerly/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/bahrus/assign-gingerly.git"
12
- },
13
- "license": "MIT",
14
- "author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
15
- "type": "module",
16
- "types": "types/assign-gingerly/types.d.ts",
17
- "files": [
18
- "*.js",
19
- "*.ts",
20
- "README.md",
21
- "LICENSE",
22
- "types/assign-gingerly/types.d.ts"
23
- ],
24
- "exports": {
25
- ".": {
26
- "default": "./index.js",
27
- "types": "./index.ts"
28
- },
29
- "./assignGingerly.js": {
30
- "default": "./assignGingerly.js",
31
- "types": "./assignGingerly.ts"
32
- },
33
- "./assignTentatively.js": {
34
- "default": "./assignTentatively.js",
35
- "types": "./assignTentatively.ts"
36
- },
37
- "./waitForEvent.js": {
38
- "default": "./waitForEvent.js"
39
- },
40
- "./parserRegistry.js": {
41
- "default": "./parserRegistry.js"
42
- },
43
- "./parseWithAttrs.js": {
44
- "default": "./parseWithAttrs.js",
45
- "types": "./parseWithAttrs.ts"
46
- },
47
- "./buildCSSQuery.js": {
48
- "default": "./buildCSSQuery.js",
49
- "types": "./buildCSSQuery.ts"
50
- },
51
- "./resolveTemplate.js": {
52
- "default": "./resolveTemplate.js",
53
- "types": "./resolveTemplate.ts"
54
- }
55
- },
56
- "main": "index.js",
57
- "module": "index.js",
58
- "scripts": {
59
- "serve": "node ./node_modules/spa-ssi/serve.js",
60
- "test": "playwright test",
61
- "update": "ncu -u && npm install",
62
- "safari": "npx playwright wk http://localhost:8000",
63
- "chrome": "npx playwright cr http://localhost:8000"
64
- },
65
- "devDependencies": {
66
- "@playwright/test": "1.59.1",
67
- "spa-ssi": "0.0.27",
68
- "@types/node": "25.5.2",
69
- "typescript": "6.0.2"
70
- }
71
- }
1
+ {
2
+ "name": "assign-gingerly",
3
+ "version": "0.0.30",
4
+ "description": "This package provides a utility function for carefully merging one object into another.",
5
+ "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/bahrus/assign-gingerly/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bahrus/assign-gingerly.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
15
+ "type": "module",
16
+ "types": "types/assign-gingerly/types.d.ts",
17
+ "files": [
18
+ "*.js",
19
+ "*.ts",
20
+ "README.md",
21
+ "LICENSE",
22
+ "types/assign-gingerly/types.d.ts"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "default": "./index.js",
27
+ "types": "./index.ts"
28
+ },
29
+ "./assignGingerly.js": {
30
+ "default": "./assignGingerly.js",
31
+ "types": "./assignGingerly.ts"
32
+ },
33
+ "./assignTentatively.js": {
34
+ "default": "./assignTentatively.js",
35
+ "types": "./assignTentatively.ts"
36
+ },
37
+ "./waitForEvent.js": {
38
+ "default": "./waitForEvent.js"
39
+ },
40
+ "./parserRegistry.js": {
41
+ "default": "./parserRegistry.js"
42
+ },
43
+ "./parseWithAttrs.js": {
44
+ "default": "./parseWithAttrs.js",
45
+ "types": "./parseWithAttrs.ts"
46
+ },
47
+ "./buildCSSQuery.js": {
48
+ "default": "./buildCSSQuery.js",
49
+ "types": "./buildCSSQuery.ts"
50
+ },
51
+ "./resolveTemplate.js": {
52
+ "default": "./resolveTemplate.js",
53
+ "types": "./resolveTemplate.ts"
54
+ },
55
+ "./getHost.js": {
56
+ "default": "./getHost.js",
57
+ "types": "./getHost.ts"
58
+ },
59
+ "./Infer.js": {
60
+ "default": "./Infer.js",
61
+ "types": "./Infer.ts"
62
+ }
63
+ },
64
+ "main": "index.js",
65
+ "module": "index.js",
66
+ "scripts": {
67
+ "serve": "node ./node_modules/spa-ssi/serve.js",
68
+ "test": "playwright test",
69
+ "update": "ncu -u && npm install",
70
+ "safari": "npx playwright wk http://localhost:8000",
71
+ "chrome": "npx playwright cr http://localhost:8000"
72
+ },
73
+ "devDependencies": {
74
+ "@playwright/test": "1.59.1",
75
+ "spa-ssi": "0.0.27",
76
+ "@types/node": "25.5.2",
77
+ "typescript": "6.0.2"
78
+ }
79
+ }
package/parseWithAttrs.js CHANGED
@@ -147,7 +147,8 @@ function hasDashOrNonASCII(str) {
147
147
  * @returns The attribute value or null
148
148
  */
149
149
  function getAttributeValue(element, attrName, allowUnprefixed) {
150
- const isCustomElement = element.tagName.includes('-');
150
+ const { localName } = element;
151
+ const isCustomElement = localName.includes('-');
151
152
  const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
152
153
  // For custom elements and SVG - strict enh- requirement
153
154
  if (isCustomElement || isSVGElement) {
@@ -157,8 +158,7 @@ function getAttributeValue(element, attrName, allowUnprefixed) {
157
158
  // Only fallback if tag name matches the allowUnprefixed pattern
158
159
  if (allowUnprefixed) {
159
160
  const pattern = typeof allowUnprefixed === 'string' ? new RegExp(allowUnprefixed) : allowUnprefixed;
160
- const tagName = element.tagName.toLowerCase();
161
- if (pattern.test(tagName)) {
161
+ if (pattern.test(localName)) {
162
162
  return element.getAttribute(attrName);
163
163
  }
164
164
  }
package/parseWithAttrs.ts CHANGED
@@ -186,7 +186,8 @@ function getAttributeValue(
186
186
  attrName: string,
187
187
  allowUnprefixed?: string | RegExp
188
188
  ): string | null {
189
- const isCustomElement = element.tagName.includes('-');
189
+ const { localName } = element;
190
+ const isCustomElement = localName.includes('-');
190
191
  const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
191
192
 
192
193
  // For custom elements and SVG - strict enh- requirement
@@ -197,8 +198,7 @@ function getAttributeValue(
197
198
  // Only fallback if tag name matches the allowUnprefixed pattern
198
199
  if (allowUnprefixed) {
199
200
  const pattern = typeof allowUnprefixed === 'string' ? new RegExp(allowUnprefixed) : allowUnprefixed;
200
- const tagName = element.tagName.toLowerCase();
201
- if (pattern.test(tagName)) {
201
+ if (pattern.test(localName)) {
202
202
  return element.getAttribute(attrName);
203
203
  }
204
204
  }
@@ -112,7 +112,7 @@ export type ParserFunction<T = any> =
112
112
  | ((attrValue: string | null) => any)
113
113
  | ((attrValue: string | null, context?: ParserContext<T>) => any);
114
114
 
115
- export interface AttrConfig<T = any> {
115
+ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
116
116
  /**
117
117
  * Type of the property value (JSON-serializable string format)
118
118
  */
@@ -152,6 +152,12 @@ export interface AttrConfig<T = any> {
152
152
  | ParserFunction<T>
153
153
  | string
154
154
  ;
155
+
156
+ /**
157
+ * configuration information needed by a custom parser to properly
158
+ * parse the attribute.
159
+ */
160
+ parserConfig?: TParserConfig;
155
161
 
156
162
  /**
157
163
  * Default value to use when attribute is missing