assign-gingerly 0.0.12 → 0.0.13

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 CHANGED
@@ -288,8 +288,10 @@ This guarantees that applying the reversal object restores the object to its exa
288
288
  interface IBaseRegistryItem<T = any> {
289
289
  spawn: {new(): T} | Promise<{new(): T}>
290
290
  symlinks: {[key: symbol]: keyof T}
291
- enhKey?: string // Optional: for element enhancement access
292
- withAttrs?: AttrPatterns<T> // Optional: automatic attribute parsing
291
+ // Optional: for element enhancement access
292
+ enhKey?: string
293
+ // Optional: automatic attribute parsing
294
+ withAttrs?: AttrPatterns<T>
293
295
  }
294
296
 
295
297
  export const isHappy = Symbol.for('TFWsx0YH5E6eSfhE7zfLxA');
@@ -349,7 +351,8 @@ It also adds a lazy property to the first passed in parameter, "set", which retu
349
351
 
350
352
  The suggestion to use Symbol.for with a guid, as opposed to just Symbol(), is based on some negative experiences I've had with multiple versions of the same library being referenced, but is not required. Regular symbols could also be used when that risk can be avoided.
351
353
 
352
- ### Global Instance Map for Cross-Version Compatibility
354
+ <details>
355
+ <summary>Global Instance Map for Cross-Version Compatibility</summary>
353
356
 
354
357
  To ensure instance uniqueness even when multiple versions of this package are loaded, spawned instances are stored in a global WeakMap at `globalThis['HDBhTPLuIUyooMxK88m68Q']`. This guarantees that:
355
358
 
@@ -443,7 +446,10 @@ console.log(target.set[symbol1].prop1); // 'value1'
443
446
  console.log(target.set[symbol1].prop2); // 'value2'
444
447
  ```
445
448
 
446
- ## Support for JSON assignment with Symbol.for symbols
449
+ </details>
450
+
451
+ <details>
452
+ <summary>Support for JSON assignment with Symbol.for symbols</summary>
447
453
 
448
454
  ```JavaScript
449
455
  const result = assignGingerly({}, {
@@ -455,8 +461,13 @@ const result = assignGingerly({}, {
455
461
  registry: BaseRegistry
456
462
  });
457
463
  ```
464
+ </details>
465
+
466
+
467
+ <!--
458
468
 
459
469
 
470
+ Already covered, I think
460
471
 
461
472
  ## Object.prototype Extensions
462
473
 
@@ -491,11 +502,14 @@ console.log(obj); // { a: 1, b: { c: 2 }, d: 3 }
491
502
 
492
503
  The prototype extensions are non-enumerable and won't appear in `Object.keys()` or `for...in` loops.
493
504
 
505
+ -->
506
+
494
507
  ## Custom Element Registry Integration (Chrome 146+)
495
508
 
496
- This package includes support for Chrome's scoped custom element registries, which automatically integrates dependency injection with custom elements.
509
+ This package includes support for Chrome's scoped custom element registries, which automatically integrates dependency injection in harmony with scoped custom elements DOM sections or ShadowRoots.
497
510
 
498
- ### Automatic Registry Population
511
+ <details>
512
+ <summary>Automatic Registry Population</summary>
499
513
 
500
514
  When `assignGingerly` or `assignTentatively` is called on an Element instance without providing an explicit `registry` option, it automatically uses the registry from `element.customElementRegistry.enhancementRegistry`:
501
515
 
@@ -523,7 +537,10 @@ myElement.assignGingerly({
523
537
  });
524
538
  ```
525
539
 
526
- ### Lazy Registry Creation
540
+ </details>
541
+
542
+ <details>
543
+ <summary>Lazy Registry Creation</summary>
527
544
 
528
545
  Each `CustomElementRegistry` instance gets its own `enhancementRegistry` property via a lazy getter. The `BaseRegistry` instance is created on first access and cached for subsequent uses:
529
546
 
@@ -538,8 +555,10 @@ const registry2 = element2.customElementRegistry.enhancementRegistry;
538
555
  // Multiple accesses return the same instance
539
556
  console.log(registry1 === element1.customElementRegistry.enhancementRegistry); // true
540
557
  ```
558
+ </details>
541
559
 
542
- ### Explicit Registry Override
560
+ <details>
561
+ <summary>Explicit Registry Override</summary>
543
562
 
544
563
  You can still provide an explicit `registry` option to override the automatic behavior:
545
564
 
@@ -549,10 +568,11 @@ const customRegistry = new BaseRegistry();
549
568
 
550
569
  myElement.assignGingerly({
551
570
  [mySymbol]: 'value'
552
- }, { registry: customRegistry }); // Uses customRegistry instead of customElementRegistry
571
+ }, { registry: customRegistry }); // Uses customRegistry instead of customElementRegistry.enhancementRegistry
553
572
  ```
554
573
 
555
574
  **Browser Support**: This feature requires Chrome 146+ with scoped custom element registry support. The implementation is designed as a polyfill for the web standards proposal and does not include fallback behavior for older browsers.
575
+ </details>
556
576
 
557
577
  ## Enhanced Element Property Assignment with `enh.set` Proxy (Chrome 146+)
558
578
 
@@ -564,7 +584,7 @@ The `enh.set` proxy allows you to assign properties to enhancements using a clea
564
584
 
565
585
  ```TypeScript
566
586
  import 'assign-gingerly/object-extension.js';
567
- import { BaseRegistry } from 'assign-gingerly';
587
+ //import { BaseRegistry } from 'assign-gingerly';
568
588
 
569
589
  // Define an enhancement class
570
590
  class MyEnhancement {
@@ -588,7 +608,6 @@ const registry = myElement.customElementRegistry.enhancementRegistry;
588
608
 
589
609
  registry.push({
590
610
  spawn: MyEnhancement,
591
- symlinks: {},
592
611
  enhKey: 'myEnh' // Key identifier for this enhancement
593
612
  });
594
613
 
@@ -601,7 +620,10 @@ console.log(myElement.enh.myEnh.myProp); // 'hello'
601
620
  console.log(myElement.enh.myEnh.element === myElement); // true
602
621
  ```
603
622
 
604
- ### How It Works
623
+ <details>
624
+ <summary>
625
+ How It Works
626
+ </summary>
605
627
 
606
628
  When you access `element.enh.set.enhKey.property`, the proxy:
607
629
 
@@ -614,12 +636,14 @@ When you access `element.enh.set.enhKey.property`, the proxy:
614
636
  3. **Reuses existing instances**: If the enhancement already exists and is the correct type, it reuses it
615
637
  4. **Falls back to plain objects**: If no registry item is found, creates a plain object at `element.enh[enhKey]`
616
638
 
639
+ </details>
640
+
617
641
  ### Why the `enh` Namespace?
618
642
 
619
643
  The `enh` property provides a dedicated namespace for enhancements, similar to how `dataset` provides a namespace for data attributes. This prevents conflicts with:
620
644
  - Future platform properties that might be added to Element
621
645
  - Existing element properties and methods
622
- - Other libraries that might extend Element
646
+ - Other libraries that might extend HTMLElement
623
647
 
624
648
  This approach is part of a proposal to WHATWG for standardizing element enhancements.
625
649
 
@@ -647,7 +671,8 @@ class Enhancement {
647
671
 
648
672
  All parameters are optional for backward compatibility with existing code.
649
673
 
650
- **Passing Custom Context:**
674
+ <details>
675
+ <summary>Passing Custom Context</summary>
651
676
 
652
677
  You can pass custom context when calling `enh.get()` or `enh.whenResolved()`:
653
678
 
@@ -673,9 +698,11 @@ This is useful for:
673
698
 
674
699
  **Note**: The `mountCtx` is only available when explicitly calling `enh.get()` or `enh.whenResolved()`. It's not available when accessing via the `enh.set` proxy (since that's a property getter with no way to pass parameters).
675
700
 
701
+ </details>
702
+
676
703
  ### Registry Item with enhKey
677
704
 
678
- Registry items now support optional `enhKey`, `withAttrs`, `canSpawn`, and `lifecycleKeys` properties:
705
+ In addition to spawn and symlinks, registry items support optional properties `enhKey`, `withAttrs`, `canSpawn`, and `lifecycleKeys`:
679
706
 
680
707
  ```TypeScript
681
708
  interface IBaseRegistryItem<T> {
@@ -683,7 +710,7 @@ interface IBaseRegistryItem<T> {
683
710
  new (oElement?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
684
711
  canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
685
712
  };
686
- symlinks: { [key: string | symbol]: keyof T };
713
+ symlinks?: { [key: string | symbol]: keyof T };
687
714
  enhKey?: string; // String identifier for set proxy access
688
715
  withAttrs?: AttrPatterns<T>; // Automatic attribute parsing during spawn
689
716
  lifecycleKeys?:
@@ -697,11 +724,14 @@ interface IBaseRegistryItem<T> {
697
724
 
698
725
  The `withAttrs` property enables automatic attribute parsing when the enhancement is spawned. See the [Parsing Attributes with parseWithAttrs](#parsing-attributes-with-parsewithattrs) section for details.
699
726
 
727
+ It also tips off extending polyfills / libraries, in particular mount-observer, to be on te lookout for the attributes specified by withAttrs. But *assign-gingerly, by itself, performs **no** DOM observing to automatically spawn the class instance*. It expects consumers of the polyfill to programmatically attach such behavior/enhancements, and/or rely on alternative, higher level packages to be vigilant for enhancement opportunities.
728
+
700
729
  The `canSpawn` static method allows enhancement classes to conditionally block spawning based on the target object. See the [Conditional Spawning with canSpawn](#conditional-spawning-with-canspawn) section for details.
701
730
 
702
731
  The `lifecycleKeys` property configures lifecycle integration without requiring base classes. See the [Lifecycle Keys: Configuration vs Convention](#lifecycle-keys-configuration-vs-convention) section for details.
703
732
 
704
- ### Advanced Examples
733
+ <details>
734
+ <summary>Advanced Examples</summary>
705
735
 
706
736
  **Multiple Enhancements:**
707
737
  ```TypeScript
@@ -724,8 +754,8 @@ const element = document.createElement('div');
724
754
  const registry = element.customElementRegistry.enhancementRegistry;
725
755
 
726
756
  registry.push([
727
- { spawn: StyleEnhancement, symlinks: {}, enhKey: 'styles' },
728
- { spawn: DataEnhancement, symlinks: {}, enhKey: 'data' }
757
+ { spawn: StyleEnhancement, enhKey: 'styles' },
758
+ { spawn: DataEnhancement, enhKey: 'data' }
729
759
  ]);
730
760
 
731
761
  element.enh.set.styles.height = '100px';
@@ -742,7 +772,6 @@ const registry = element.customElementRegistry.enhancementRegistry;
742
772
 
743
773
  registry.push({
744
774
  spawn: MyEnhancement,
745
- symlinks: {},
746
775
  enhKey: 'config'
747
776
  });
748
777
 
@@ -768,7 +797,10 @@ element.enh.set.plainData.prop2 = 'value2';
768
797
  console.log(element.enh.plainData); // { prop1: 'value1', prop2: 'value2' }
769
798
  ```
770
799
 
771
- ### Finding Registry Items by enhKey
800
+ </details>
801
+
802
+ <details>
803
+ <summary>Finding Registry Items by enhKey</summary>
772
804
 
773
805
  The `BaseRegistry` class includes a `findByEnhKey` method:
774
806
 
@@ -776,22 +808,21 @@ The `BaseRegistry` class includes a `findByEnhKey` method:
776
808
  const registry = new BaseRegistry();
777
809
  registry.push({
778
810
  spawn: MyEnhancement,
779
- symlinks: {},
780
811
  enhKey: 'myEnh'
781
812
  });
782
813
 
783
814
  const item = registry.findByEnhKey('myEnh');
784
815
  console.log(item.enhKey); // 'myEnh'
785
816
  ```
817
+ </details>
786
818
 
787
819
  ### Programmatic Instance Spawning with `enh.get()`
788
820
 
789
- The `enh.get()` method provides a programmatic way to spawn or retrieve enhancement instances:
821
+ The `enh.get(registryItem)` method provides a programmatic way to spawn or retrieve enhancement instances:
790
822
 
791
823
  ```TypeScript
792
824
  const registryItem = {
793
825
  spawn: MyEnhancement,
794
- symlinks: {},
795
826
  enhKey: 'myEnh'
796
827
  };
797
828
 
@@ -814,7 +845,9 @@ console.log(element.enh.myEnh === instance); // true
814
845
  - **Shared instances**: Uses the same global instance map as `assignGingerly` and `enh.set`, ensuring only one instance per registry item
815
846
  - **Auto-registration**: Automatically adds registry items to the element's registry if not present
816
847
 
817
- **Example with shared instances:**
848
+ <details>
849
+ <summary>Example with shared instances</summary>
850
+
818
851
  ```TypeScript
819
852
  const registryItem = {
820
853
  spawn: MyEnhancement,
@@ -837,7 +870,10 @@ console.log(element.enh.myEnh.prop2); // 'from set'
837
870
  console.log(element.enh.myEnh.value); // 'from assign'
838
871
  ```
839
872
 
840
- ### Lifecycle Keys: Configuration vs Convention
873
+ </details>
874
+
875
+ <details>
876
+ <summary>Lifecycle Keys: Configuration vs Convention</summary>
841
877
 
842
878
  Enhancement classes can integrate with the lifecycle system through configurable method/property names, avoiding the need for base classes or mixins.
843
879
 
@@ -857,7 +893,6 @@ For convenience, you can use `lifecycleKeys: true` to adopt standard naming conv
857
893
  ```TypeScript
858
894
  const registryItem = {
859
895
  spawn: MyEnhancement,
860
- symlinks: {},
861
896
  enhKey: 'myEnh',
862
897
  lifecycleKeys: true // Uses standard names: "dispose" and "resolved"
863
898
  };
@@ -901,7 +936,6 @@ class MyEnhancement {
901
936
 
902
937
  const registryItem = {
903
938
  spawn: MyEnhancement,
904
- symlinks: {},
905
939
  enhKey: 'myEnh',
906
940
  lifecycleKeys: {
907
941
  dispose: DISPOSE,
@@ -912,6 +946,8 @@ const registryItem = {
912
946
 
913
947
  Note: Symbol event names are not yet supported by the platform but have been requested. When supported, the `resolved` key will work as both property name and event name.
914
948
 
949
+ </details>
950
+
915
951
  ### Disposing Enhancement Instances with `enh.dispose()`
916
952
 
917
953
  The `enh.dispose()` method provides a way to clean up and remove enhancement instances:
@@ -935,7 +971,6 @@ class MyEnhancement {
935
971
 
936
972
  const registryItem = {
937
973
  spawn: MyEnhancement,
938
- symlinks: {},
939
974
  enhKey: 'myEnh',
940
975
  lifecycleKeys: true // Standard: calls dispose() method
941
976
  };
@@ -943,7 +978,6 @@ const registryItem = {
943
978
  // Or with custom name:
944
979
  const customRegistryItem = {
945
980
  spawn: MyEnhancement,
946
- symlinks: {},
947
981
  enhKey: 'myEnh',
948
982
  lifecycleKeys: {
949
983
  dispose: 'cleanup' // Custom: calls cleanup() method
@@ -994,7 +1028,6 @@ class TimerEnhancement {
994
1028
 
995
1029
  const registryItem = {
996
1030
  spawn: TimerEnhancement,
997
- symlinks: {},
998
1031
  enhKey: 'timer',
999
1032
  lifecycleKeys: true // Standard: calls dispose() method
1000
1033
  };
@@ -1041,7 +1074,6 @@ class AsyncEnhancement extends EventTarget {
1041
1074
 
1042
1075
  const registryItem = {
1043
1076
  spawn: AsyncEnhancement,
1044
- symlinks: {},
1045
1077
  enhKey: 'asyncEnh',
1046
1078
  lifecycleKeys: true // Standard: watches "resolved" property and event
1047
1079
  };
@@ -1049,7 +1081,6 @@ const registryItem = {
1049
1081
  // Or with custom name:
1050
1082
  const customRegistryItem = {
1051
1083
  spawn: AsyncEnhancement,
1052
- symlinks: {},
1053
1084
  enhKey: 'asyncEnh',
1054
1085
  lifecycleKeys: {
1055
1086
  resolved: 'isReady' // Custom: watches "isReady" property and event
@@ -1066,7 +1097,8 @@ const instanceWithContext = await element.enh.whenResolved(registryItem, authCon
1066
1097
  // The constructor receives authContext via ctx.mountCtx
1067
1098
  ```
1068
1099
 
1069
- **How `enh.whenResolved()` works:**
1100
+ <details>
1101
+ <summary>How `enh.whenResolved()` works:</summary>
1070
1102
 
1071
1103
  1. **Validates configuration**: Throws error if `lifecycleKeys.resolved` is not specified
1072
1104
  2. **Gets instance**: Calls `enh.get()` to get or spawn the instance
@@ -1127,7 +1159,6 @@ class DataEnhancement extends EventTarget {
1127
1159
 
1128
1160
  const registryItem = {
1129
1161
  spawn: DataEnhancement,
1130
- symlinks: {},
1131
1162
  enhKey: 'data',
1132
1163
  lifecycleKeys: true // Standard: watches "resolved" property and event
1133
1164
  };
@@ -1154,6 +1185,8 @@ console.log(instance1 === instance2); // true - same instance
1154
1185
 
1155
1186
  **Browser Support**: This feature requires Chrome 146+ with scoped custom element registry support.
1156
1187
 
1188
+ </details>
1189
+
1157
1190
  ## Conditional Spawning with `canSpawn`
1158
1191
 
1159
1192
  Enhancement classes can implement a static `canSpawn` method to conditionally block spawning based on the target object. This is useful for:
@@ -1187,7 +1220,6 @@ class DivOnlyEnhancement {
1187
1220
  const registry = new BaseRegistry();
1188
1221
  registry.push({
1189
1222
  spawn: DivOnlyEnhancement,
1190
- symlinks: {},
1191
1223
  enhKey: 'divOnly'
1192
1224
  });
1193
1225
 
@@ -1203,7 +1235,8 @@ const spanInstance = span.enh.get(registry.getItems()[0]);
1203
1235
  console.log(spanInstance); // undefined
1204
1236
  ```
1205
1237
 
1206
- ### How It Works
1238
+ <details>
1239
+ <summary>How It Works</summary>
1207
1240
 
1208
1241
  1. **Called before spawning**: When an enhancement is about to be spawned (via `assignGingerly`, `enh.get()`, or `enh.set`), the `canSpawn` method is called first
1209
1242
  2. **Receives context**: The method receives the target object and spawn context with registry item information
@@ -1314,6 +1347,8 @@ assignGingerly(element, { [enhSymbol]: 'test' }, { registry });
1314
1347
  // Enhancement created and value set
1315
1348
  ```
1316
1349
 
1350
+ </details>
1351
+
1317
1352
  ## Parsing Attributes with `parseWithAttrs`
1318
1353
 
1319
1354
  The `parseWithAttrs` function provides a declarative way to read and parse HTML attributes into structured data objects. It's particularly useful for custom elements and web components that need to extract configuration from attributes.
@@ -1322,19 +1357,21 @@ The `parseWithAttrs` function provides a declarative way to read and parse HTML
1322
1357
 
1323
1358
  **Important**: When using the `enh.get()`, `enh.set`, or `assignGingerly()` methods with registry items, you typically **do not need to call `parseWithAttrs()` manually**. The attribute parsing happens automatically during enhancement spawning when you include a `withAttrs` property in your registry item.
1324
1359
 
1360
+ ```html
1361
+ <my-element my-enhancement-count="42" my-enhancement-theme="dark"></my-element>
1362
+ ```
1363
+
1325
1364
  ```TypeScript
1326
1365
  import 'assign-gingerly/object-extension.js';
1327
1366
 
1328
- // HTML: <my-element data-count="42" data-theme="dark"></my-element>
1329
-
1330
1367
  class MyEnhancement {
1331
- element;
1368
+ elementRef;
1332
1369
  ctx;
1333
1370
  count = 0;
1334
1371
  theme = 'light';
1335
1372
 
1336
1373
  constructor(oElement, ctx, initVals) {
1337
- this.element = oElement;
1374
+ this.element = new WeakRef(oElement);
1338
1375
  this.ctx = ctx;
1339
1376
  // initVals automatically contains parsed attributes!
1340
1377
  if (initVals) {
@@ -1344,23 +1381,21 @@ class MyEnhancement {
1344
1381
  }
1345
1382
 
1346
1383
  const element = document.querySelector('my-element');
1347
- const registry = element.customElementRegistry.enhancementRegistry;
1348
-
1349
- // Define withAttrs in the registry item
1350
- registry.push({
1384
+ const enhancementConfig = {
1351
1385
  spawn: MyEnhancement,
1352
- symlinks: {},
1353
1386
  enhKey: 'myEnh',
1354
1387
  withAttrs: {
1355
- base: 'data-',
1356
- count: '${base}count',
1388
+ base: 'my-enhancement',
1389
+ count: '${base}-count',
1357
1390
  _count: { instanceOf: 'Number' },
1358
- theme: '${base}theme'
1391
+ theme: '${base}-theme'
1392
+
1359
1393
  }
1360
- });
1394
+ };
1395
+
1361
1396
 
1362
1397
  // Spawn the enhancement - attributes are automatically parsed!
1363
- const instance = element.enh.get(registry.getItems()[0]);
1398
+ const instance = element.enh.get(enhancementConfig);
1364
1399
  console.log(instance.count); // 42 (parsed from attribute)
1365
1400
  console.log(instance.theme); // 'dark' (parsed from attribute)
1366
1401
  ```
@@ -1374,9 +1409,7 @@ console.log(instance.theme); // 'dark' (parsed from attribute)
1374
1409
 
1375
1410
  **Precedence**: If both parsed attributes and existing `element.enh[enhKey]` values exist, the existing values take precedence over parsed attributes.
1376
1411
 
1377
- ### Manual Usage
1378
1412
 
1379
- While automatic parsing is the recommended approach, you can also call `parseWithAttrs()` manually when needed:
1380
1413
 
1381
1414
  ### The `enh-` Prefix for Attribute Isolation
1382
1415
 
@@ -1412,7 +1445,6 @@ For custom elements and SVG, you can opt-in to reading unprefixed attributes by
1412
1445
  // Allow unprefixed for elements matching pattern
1413
1446
  registry.push({
1414
1447
  spawn: MyEnhancement,
1415
- symlinks: {},
1416
1448
  enhKey: 'myEnh',
1417
1449
  allowUnprefixed: '^my-', // Only for elements starting with "my-"
1418
1450
  withAttrs: {
@@ -1425,7 +1457,6 @@ registry.push({
1425
1457
  // Or use RegExp for more complex patterns
1426
1458
  registry.push({
1427
1459
  spawn: MyEnhancement,
1428
- symlinks: {},
1429
1460
  enhKey: 'myEnh',
1430
1461
  allowUnprefixed: /^(my-|app-)/, // For "my-*" or "app-*" elements
1431
1462
  withAttrs: {
@@ -1436,7 +1467,23 @@ registry.push({
1436
1467
  });
1437
1468
  ```
1438
1469
 
1439
- When calling `parseWithAttrs()` manually, pass the pattern as the third parameter:
1470
+ <details>
1471
+ <summary>Why use `enh-` prefix?</summary>
1472
+
1473
+ 1. **Avoid conflicts**: Custom elements may use unprefixed attributes for their own purposes
1474
+ 2. **Clear intent**: Makes it obvious which attributes are for enhancements
1475
+ 3. **Future-proof**: Protects against future attribute additions to custom elements
1476
+ 4. **Consistency**: Provides a standard convention across all enhanced elements
1477
+ 5. **Selective override**: Pattern-based `allowUnprefixed` lets you opt-in specific element families while maintaining strict isolation for others
1478
+
1479
+ </details>
1480
+
1481
+ <details>
1482
+ <summary>Manual Usage</summary>
1483
+
1484
+ While automatic parsing is the recommended approach, you can also call `parseWithAttrs()` manually when needed.
1485
+
1486
+ When calling `parseWithAttrs()` manually, pass the pattern as the third (optional) parameter:
1440
1487
 
1441
1488
  ```TypeScript
1442
1489
  // Allow unprefixed only for elements matching pattern
@@ -1475,27 +1522,6 @@ const result2 = parseWithAttrs(
1475
1522
  // result2.count = undefined (unprefixed ignored because tag doesn't match)
1476
1523
  ```
1477
1524
 
1478
- **Base Attribute Validation:**
1479
-
1480
- The `base` attribute must contain either a dash (`-`) or a non-ASCII character to prevent conflicts with native attributes:
1481
-
1482
- ```TypeScript
1483
- // Valid base attributes
1484
- parseWithAttrs(element, { base: 'data-config' }); // Has dash
1485
- parseWithAttrs(element, { base: '🎨-theme' }); // Has non-ASCII
1486
-
1487
- // Invalid - throws error
1488
- parseWithAttrs(element, { base: 'config' }); // No dash or non-ASCII
1489
- ```
1490
-
1491
- **Why use `enh-` prefix?**
1492
-
1493
- 1. **Avoid conflicts**: Custom elements may use unprefixed attributes for their own purposes
1494
- 2. **Clear intent**: Makes it obvious which attributes are for enhancements
1495
- 3. **Future-proof**: Protects against future attribute additions to custom elements
1496
- 4. **Consistency**: Provides a standard convention across all enhanced elements
1497
- 5. **Selective override**: Pattern-based `allowUnprefixed` lets you opt-in specific element families while maintaining strict isolation for others
1498
-
1499
1525
  ### Basic Usage
1500
1526
 
1501
1527
  ```TypeScript
@@ -1512,7 +1538,59 @@ const config = parseWithAttrs(element, {
1512
1538
  });
1513
1539
  ```
1514
1540
 
1515
- ### AttrPatterns Configuration
1541
+ ### Error Handling
1542
+
1543
+ The function throws descriptive errors for common issues:
1544
+
1545
+ ```TypeScript
1546
+ // Circular reference
1547
+ parseWithAttrs(element, {
1548
+ a: '${b}',
1549
+ b: '${a}' // Error: Circular reference detected
1550
+ });
1551
+
1552
+ // Undefined variable
1553
+ parseWithAttrs(element, {
1554
+ name: '${missing}' // Error: Undefined template variable: missing
1555
+ });
1556
+
1557
+ // Invalid JSON
1558
+ // HTML: <div data-obj='{invalid}'></div>
1559
+ parseWithAttrs(element, {
1560
+ base: 'data-',
1561
+ obj: '${base}obj',
1562
+ _obj: { instanceOf: 'Object' }
1563
+ // Error: Failed to parse JSON: "{invalid}"
1564
+ });
1565
+
1566
+ // Invalid number
1567
+ // HTML: <div data-count="abc"></div>
1568
+ parseWithAttrs(element, {
1569
+ base: 'data-',
1570
+ count: '${base}count',
1571
+ _count: { instanceOf: 'Number' }
1572
+ // Error: Failed to parse number: "abc"
1573
+ });
1574
+ ```
1575
+
1576
+ </details>
1577
+
1578
+ **Base Attribute Validation:**
1579
+
1580
+ The `base` attribute must contain either a dash (`-`) or a non-ASCII character to prevent conflicts with native attributes:
1581
+
1582
+ ```TypeScript
1583
+ // Valid base attributes
1584
+ const enhConfig1 = { base: 'data-config' }; // Has dash
1585
+ const enhConfig2 = { base: '🎨-theme' }); // Has non-ASCII (and dash)
1586
+
1587
+ // Invalid - throws error
1588
+ const enhConig3 = { base: 'config' }; // No dash or non-ASCII
1589
+ ```
1590
+
1591
+
1592
+ <details>
1593
+ <summary>AttrPatterns Configuration</summary>
1516
1594
 
1517
1595
  The `parseWithAttrs` function accepts an `AttrPatterns` object that defines:
1518
1596
 
@@ -1597,6 +1675,139 @@ const result = parseWithAttrs(element, {
1597
1675
  // Result: { createdAt: 1705315800000 }
1598
1676
  ```
1599
1677
 
1678
+ ### Named Parsers for Reusability and JSON Serialization
1679
+
1680
+ Instead of inline functions, you can reference parsers by name, making configs JSON serializable and parsers reusable:
1681
+
1682
+ ```TypeScript
1683
+ import { globalParserRegistry, parseWithAttrs } from 'assign-gingerly';
1684
+
1685
+ // Register parsers once (typically in app initialization)
1686
+ globalParserRegistry.register('timestamp', (v) =>
1687
+ v ? new Date(v).getTime() : null
1688
+ );
1689
+
1690
+ globalParserRegistry.register('csv', (v) =>
1691
+ v ? v.split(',').map(s => s.trim()) : []
1692
+ );
1693
+
1694
+ // Use by name - config is now JSON serializable!
1695
+ const config = {
1696
+ base: 'data-',
1697
+ created: '${base}created',
1698
+ _created: {
1699
+ parser: 'timestamp' // String reference instead of function
1700
+ },
1701
+ tags: '${base}tags',
1702
+ _tags: {
1703
+ parser: 'csv'
1704
+ }
1705
+ };
1706
+
1707
+ // Can serialize to JSON
1708
+ const json = JSON.stringify(config);
1709
+
1710
+ // Use the config
1711
+ const result = parseWithAttrs(element, config);
1712
+ ```
1713
+
1714
+ **Built-in Named Parsers:**
1715
+
1716
+ The following parsers are pre-registered in `globalParserRegistry`:
1717
+
1718
+ - `'timestamp'` - Parses ISO date string to Unix timestamp (milliseconds)
1719
+ - `'date'` - Parses string to Date object
1720
+ - `'csv'` - Splits comma-separated values into trimmed array
1721
+ - `'int'` - Parses integer with `parseInt(v, 10)`
1722
+ - `'float'` - Parses float with `parseFloat(v)`
1723
+ - `'boolean'` - Presence check (same as `instanceOf: 'Boolean'`)
1724
+ - `'json'` - Parses JSON (same as `instanceOf: 'Object'` or `'Array'`)
1725
+
1726
+ **Custom Element Static Method Parsers:**
1727
+
1728
+ You can also reference static methods on custom elements using dot notation:
1729
+
1730
+ ```TypeScript
1731
+ class MyWidget extends HTMLElement {
1732
+ static parseSpecialFormat(v) {
1733
+ return v ? v.toUpperCase() : null;
1734
+ }
1735
+
1736
+ static parseWithPrefix(v) {
1737
+ return v ? `PREFIX:${v}` : null;
1738
+ }
1739
+ }
1740
+ customElements.define('my-widget', MyWidget);
1741
+
1742
+ // Reference custom element parsers
1743
+ const config = {
1744
+ base: 'data-',
1745
+ value: '${base}value',
1746
+ _value: {
1747
+ parser: 'my-widget.parseSpecialFormat' // element-name.methodName
1748
+ }
1749
+ };
1750
+ ```
1751
+
1752
+ **Parser Resolution Order:**
1753
+
1754
+ When a string parser is specified:
1755
+
1756
+ 1. **Check for dot notation** - If parser contains `.`, try to resolve as `element-name.methodName`
1757
+ 2. **Try custom element** - Look up element in `customElements` registry and check for static method
1758
+ 3. **Fall back to global registry** - If custom element not found, check `globalParserRegistry`
1759
+ 4. **Throw error** - If not found anywhere, throw descriptive error
1760
+
1761
+ This allows:
1762
+ - Element-specific parsers to be scoped to their custom elements
1763
+ - Fallback to global registry for shared parsers
1764
+ - Dot notation in global registry names (e.g., `'utils.parseDate'`)
1765
+
1766
+ **Example: Organizing Parsers**
1767
+
1768
+ ```TypeScript
1769
+ // parsers.js - Centralized parser definitions
1770
+ export function registerCommonParsers(registry) {
1771
+ registry.register('uppercase', (v) => v ? v.toUpperCase() : null);
1772
+ registry.register('lowercase', (v) => v ? v.toLowerCase() : null);
1773
+ registry.register('trim', (v) => v ? v.trim() : null);
1774
+ registry.register('phone', (v) => v ? v.replace(/\D/g, '') : null);
1775
+ }
1776
+
1777
+ // app.js - Register at startup
1778
+ import { globalParserRegistry } from 'assign-gingerly';
1779
+ import { registerCommonParsers } from './parsers.js';
1780
+
1781
+ registerCommonParsers(globalParserRegistry);
1782
+
1783
+ // Now all configs can use these parsers by name
1784
+ ```
1785
+
1786
+ **Benefits of Named Parsers:**
1787
+
1788
+ - ✅ **JSON serializable** - Configs can be stored/transmitted as JSON
1789
+ - ✅ **Reusable** - Define once, use everywhere
1790
+ - ✅ **Maintainable** - Update parser logic in one place
1791
+ - ✅ **Testable** - Test parsers independently
1792
+ - ✅ **Discoverable** - `globalParserRegistry.getNames()` lists all available parsers
1793
+ - ✅ **Backward compatible** - Inline functions still work
1794
+
1795
+ **Mixing Inline and Named Parsers:**
1796
+
1797
+ ```TypeScript
1798
+ const config = {
1799
+ base: 'data-',
1800
+ created: '${base}created',
1801
+ _created: {
1802
+ parser: 'timestamp' // Named parser
1803
+ },
1804
+ special: '${base}special',
1805
+ _special: {
1806
+ parser: (v) => v ? v.split('').reverse().join('') : null // Inline
1807
+ }
1808
+ };
1809
+ ```
1810
+
1600
1811
  ### Property Mapping with mapsTo
1601
1812
 
1602
1813
  The `mapsTo` property controls where parsed values are placed:
@@ -1630,6 +1841,250 @@ const result = parseWithAttrs(element, {
1630
1841
  // Result: { theme: 'dark', lang: 'en' }
1631
1842
  ```
1632
1843
 
1844
+ ### Default Values with valIfNull
1845
+
1846
+ The `valIfNull` property allows you to specify default values when attributes are missing:
1847
+
1848
+ ```TypeScript
1849
+ // HTML: <div></div> (no attributes)
1850
+
1851
+ const result = parseWithAttrs(element, {
1852
+ base: 'data-',
1853
+ theme: '${base}theme',
1854
+ _theme: {
1855
+ instanceOf: 'String',
1856
+ valIfNull: 'light' // Default when attribute is missing
1857
+ },
1858
+ count: '${base}count',
1859
+ _count: {
1860
+ instanceOf: 'Number',
1861
+ valIfNull: 0 // Default to 0
1862
+ }
1863
+ });
1864
+ // Result: { theme: 'light', count: 0 }
1865
+ ```
1866
+
1867
+ **How it works:**
1868
+ - **Attribute missing**: If the attribute doesn't exist and `valIfNull` is defined, the default value is used **without calling the parser**
1869
+ - **Attribute present**: If the attribute exists (even if empty string), the parser is called normally and `valIfNull` is ignored
1870
+ - **No valIfNull**: If `valIfNull` is undefined and the attribute is missing, the property is not added to the result (current behavior)
1871
+
1872
+ **Important notes:**
1873
+ 1. **Parser is bypassed**: When `valIfNull` is used, the parser is NOT called - the default value is used as-is
1874
+ 2. **Empty string vs missing**: `valIfNull` only applies when the attribute is completely absent. If the attribute exists but is empty (`data-count=""`), the parser IS called
1875
+ 3. **Any value allowed**: `valIfNull` can be any JavaScript value: string, number, boolean, object, array, null, etc.
1876
+ 4. **Falsy values work**: Even falsy values like `0`, `false`, `''`, or `null` are valid defaults
1877
+
1878
+ **Examples with different types:**
1879
+
1880
+ ```TypeScript
1881
+ // Object default
1882
+ const result1 = parseWithAttrs(element, {
1883
+ base: 'config-',
1884
+ settings: '${base}settings',
1885
+ _settings: {
1886
+ instanceOf: 'Object',
1887
+ valIfNull: { enabled: false, mode: 'auto' }
1888
+ }
1889
+ });
1890
+ // Result: { settings: { enabled: false, mode: 'auto' } }
1891
+
1892
+ // Boolean default
1893
+ const result2 = parseWithAttrs(element, {
1894
+ base: 'feature-',
1895
+ enabled: '${base}enabled',
1896
+ _enabled: {
1897
+ instanceOf: 'Boolean',
1898
+ valIfNull: false
1899
+ }
1900
+ });
1901
+ // Result: { enabled: false }
1902
+
1903
+ // Array default
1904
+ const result3 = parseWithAttrs(element, {
1905
+ base: 'data-',
1906
+ items: '${base}items',
1907
+ _items: {
1908
+ instanceOf: 'Array',
1909
+ valIfNull: []
1910
+ }
1911
+ });
1912
+ // Result: { items: [] }
1913
+
1914
+ // null as default
1915
+ const result4 = parseWithAttrs(element, {
1916
+ base: 'data-',
1917
+ value: '${base}value',
1918
+ _value: {
1919
+ instanceOf: 'String',
1920
+ valIfNull: null
1921
+ }
1922
+ });
1923
+ // Result: { value: null }
1924
+ ```
1925
+
1926
+ **Comparison: Empty string vs missing attribute:**
1927
+
1928
+ ```html
1929
+ <!-- Attribute is missing -->
1930
+ <div></div>
1931
+
1932
+ <!-- Attribute exists but is empty -->
1933
+ <div data-count=""></div>
1934
+ ```
1935
+
1936
+ ```TypeScript
1937
+ const config = {
1938
+ base: 'data-',
1939
+ count: '${base}count',
1940
+ _count: {
1941
+ instanceOf: 'Number',
1942
+ valIfNull: 99
1943
+ }
1944
+ };
1945
+
1946
+ // Missing attribute - uses valIfNull
1947
+ const result1 = parseWithAttrs(document.querySelector('div:nth-child(1)'), config);
1948
+ // Result: { count: 99 }
1949
+
1950
+ // Empty string - calls parser (returns null for empty Number)
1951
+ const result2 = parseWithAttrs(document.querySelector('div:nth-child(2)'), config);
1952
+ // Result: { count: null }
1953
+ ```
1954
+
1955
+ ### Performance Optimization with parseCache
1956
+
1957
+ The `parseCache` property enables caching of parsed attribute values to improve performance when the same attribute values appear repeatedly throughout the document:
1958
+
1959
+ ```TypeScript
1960
+ // HTML: Multiple elements with same attribute values
1961
+ // <div data-config='{"theme":"dark","size":"large"}'></div>
1962
+ // <div data-config='{"theme":"dark","size":"large"}'></div>
1963
+ // <div data-config='{"theme":"dark","size":"large"}'></div>
1964
+
1965
+ const config = {
1966
+ base: 'data-',
1967
+ config: '${base}config',
1968
+ _config: {
1969
+ instanceOf: 'Object',
1970
+ parseCache: 'shared' // Cache and reuse parsed objects
1971
+ }
1972
+ };
1973
+
1974
+ // First parse - parses and caches
1975
+ const result1 = parseWithAttrs(element1, config);
1976
+
1977
+ // Subsequent parses - returns cached value (no parsing)
1978
+ const result2 = parseWithAttrs(element2, config);
1979
+ const result3 = parseWithAttrs(element3, config);
1980
+ ```
1981
+
1982
+ **Cache Strategies:**
1983
+
1984
+ 1. **`'shared'`**: Returns the same object reference from cache
1985
+ - **Fastest**: No cloning overhead
1986
+ - **Risk**: Enhancements that mutate the object will affect all instances
1987
+ - **Best for**: Immutable data or when you trust enhancements not to mutate
1988
+
1989
+ 2. **`'cloned'`**: Returns a structural clone of the cached object
1990
+ - **Safer**: Each instance gets its own copy
1991
+ - **Slower**: Uses `structuredClone()` which has overhead
1992
+ - **Best for**: Mutable data or when enhancements might modify values
1993
+
1994
+ **Examples:**
1995
+
1996
+ ```TypeScript
1997
+ // Shared cache - fast but requires discipline
1998
+ const sharedConfig = {
1999
+ base: 'data-',
2000
+ settings: '${base}settings',
2001
+ _settings: {
2002
+ instanceOf: 'Object',
2003
+ parseCache: 'shared' // All instances share same object
2004
+ }
2005
+ };
2006
+
2007
+ // Cloned cache - safer for mutable data
2008
+ const clonedConfig = {
2009
+ base: 'data-',
2010
+ state: '${base}state',
2011
+ _state: {
2012
+ instanceOf: 'Object',
2013
+ parseCache: 'cloned' // Each instance gets a copy
2014
+ }
2015
+ };
2016
+
2017
+ // Custom parser with caching
2018
+ let parseCount = 0;
2019
+ const customConfig = {
2020
+ base: 'data-',
2021
+ timestamp: '${base}timestamp',
2022
+ _timestamp: {
2023
+ parser: (v) => {
2024
+ parseCount++; // Track parse calls
2025
+ return v ? new Date(v).getTime() : null;
2026
+ },
2027
+ parseCache: 'shared' // Parser only called once per unique value
2028
+ }
2029
+ };
2030
+ ```
2031
+
2032
+ **Important Notes:**
2033
+
2034
+ 1. **Parser purity**: Parsers should be pure functions (no side effects) when using caching
2035
+ 2. **Boolean types**: Caching is skipped for Boolean types (presence check doesn't benefit)
2036
+ 3. **Cache scope**: Cache is module-level and persists across all `parseWithAttrs()` calls
2037
+ 4. **Cache key**: Values are cached per `(instanceOf, parserType, attributeValue)` tuple
2038
+ 5. **Memory**: Cache grows with unique attribute values encountered (no automatic cleanup)
2039
+ 6. **Browser support**: `'cloned'` strategy requires `structuredClone()` (modern browsers)
2040
+
2041
+ **Performance Considerations:**
2042
+
2043
+ - **Shared cache**: Best for simple objects, arrays, or when parsing is expensive
2044
+ - **Cloned cache**: Overhead may negate benefits for simple values (strings, numbers)
2045
+ - **No cache**: Better for unique values or when parsing is trivial
2046
+ - **Custom parsers**: Caching is most beneficial when parser does expensive operations (Date parsing, complex transformations)
2047
+
2048
+ **Example: Shared cache mutation risk**
2049
+
2050
+ ```TypeScript
2051
+ const config = {
2052
+ base: 'data-',
2053
+ items: '${base}items',
2054
+ _items: {
2055
+ instanceOf: 'Array',
2056
+ parseCache: 'shared'
2057
+ }
2058
+ };
2059
+
2060
+ // HTML: <div data-items='[1,2,3]'></div>
2061
+
2062
+ const result1 = parseWithAttrs(element1, config);
2063
+ result1.items.push(4); // Mutation!
2064
+
2065
+ const result2 = parseWithAttrs(element2, config);
2066
+ console.log(result2.items); // [1,2,3,4] - mutation is visible!
2067
+ ```
2068
+
2069
+ **Example: Cloned cache safety**
2070
+
2071
+ ```TypeScript
2072
+ const config = {
2073
+ base: 'data-',
2074
+ items: '${base}items',
2075
+ _items: {
2076
+ instanceOf: 'Array',
2077
+ parseCache: 'cloned' // Safe from mutations
2078
+ }
2079
+ };
2080
+
2081
+ const result1 = parseWithAttrs(element1, config);
2082
+ result1.items.push(4); // Mutation
2083
+
2084
+ const result2 = parseWithAttrs(element2, config);
2085
+ console.log(result2.items); // [1,2,3] - original value preserved
2086
+ ```
2087
+
1633
2088
  ### Base Attribute
1634
2089
 
1635
2090
  The special `base` property handles a single attribute that spreads into the result:
@@ -1654,6 +2109,15 @@ const result2 = parseWithAttrs(element, {
1654
2109
  // Result: { greetings: { hello: 'world', goodbye: 'Mars' } }
1655
2110
  ```
1656
2111
 
2112
+ ### Best Practices
2113
+
2114
+ 1. **Use base for common prefixes**: Reduces repetition in attribute names
2115
+ 2. **Leverage template variables**: Build complex attribute names from simple parts
2116
+ 3. **Specify instanceOf**: Ensures proper type conversion
2117
+ 4. **Use mapsTo for clarity**: Map attribute names to meaningful property names
2118
+ 5. **Combine with assignGingerly**: Use nested paths (`?.`) for deep property assignment
2119
+ 6. **Handle missing attributes**: Non-existent attributes are skipped (except Boolean types)
2120
+
1657
2121
  ### Nested Paths with assignGingerly
1658
2122
 
1659
2123
  Combine `parseWithAttrs` with `assignGingerly` for nested property assignment:
@@ -1680,6 +2144,10 @@ assignGingerly(element, attrs);
1680
2144
  // element.moods.personIsHappy === true
1681
2145
  ```
1682
2146
 
2147
+ </details>
2148
+
2149
+ <!--
2150
+
1683
2151
  ### Complete Example
1684
2152
 
1685
2153
  ```TypeScript
@@ -1719,46 +2187,8 @@ console.log(result);
1719
2187
  // }
1720
2188
  ```
1721
2189
 
1722
- ### Error Handling
1723
-
1724
- The function throws descriptive errors for common issues:
1725
-
1726
- ```TypeScript
1727
- // Circular reference
1728
- parseWithAttrs(element, {
1729
- a: '${b}',
1730
- b: '${a}' // Error: Circular reference detected
1731
- });
1732
-
1733
- // Undefined variable
1734
- parseWithAttrs(element, {
1735
- name: '${missing}' // Error: Undefined template variable: missing
1736
- });
2190
+ -->
1737
2191
 
1738
- // Invalid JSON
1739
- // HTML: <div data-obj='{invalid}'></div>
1740
- parseWithAttrs(element, {
1741
- base: 'data-',
1742
- obj: '${base}obj',
1743
- _obj: { instanceOf: 'Object' }
1744
- // Error: Failed to parse JSON: "{invalid}"
1745
- });
1746
2192
 
1747
- // Invalid number
1748
- // HTML: <div data-count="abc"></div>
1749
- parseWithAttrs(element, {
1750
- base: 'data-',
1751
- count: '${base}count',
1752
- _count: { instanceOf: 'Number' }
1753
- // Error: Failed to parse number: "abc"
1754
- });
1755
- ```
1756
2193
 
1757
- ### Best Practices
1758
2194
 
1759
- 1. **Use base for common prefixes**: Reduces repetition in attribute names
1760
- 2. **Leverage template variables**: Build complex attribute names from simple parts
1761
- 3. **Specify instanceOf**: Ensures proper type conversion
1762
- 4. **Use mapsTo for clarity**: Map attribute names to meaningful property names
1763
- 5. **Combine with assignGingerly**: Use nested paths (`?.`) for deep property assignment
1764
- 6. **Handle missing attributes**: Non-existent attributes are skipped (except Boolean types)