assign-gingerly 0.0.11 → 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
 
@@ -628,26 +652,57 @@ This approach is part of a proposal to WHATWG for standardizing element enhancem
628
652
  Enhancement classes should follow this constructor signature:
629
653
 
630
654
  ```TypeScript
631
- interface SpawnContext<T> {
655
+ interface SpawnContext<T, TMountContext = any> {
632
656
  config: IBaseRegistryItem<T>;
657
+ mountCtx?: TMountContext; // Optional custom context passed by caller
633
658
  }
634
659
 
635
660
  class Enhancement {
636
661
  constructor(
637
662
  oElement?: Element, // The element being enhanced
638
- ctx?: SpawnContext, // Context with registry item info
663
+ ctx?: SpawnContext, // Context with registry item info and optional mountCtx
639
664
  initVals?: Partial<T> // Initial values if property existed
640
665
  ) {
641
666
  // Your initialization logic
667
+ // Access custom context via ctx.mountCtx if provided
642
668
  }
643
669
  }
644
670
  ```
645
671
 
646
672
  All parameters are optional for backward compatibility with existing code.
647
673
 
674
+ <details>
675
+ <summary>Passing Custom Context</summary>
676
+
677
+ You can pass custom context when calling `enh.get()` or `enh.whenResolved()`:
678
+
679
+ ```TypeScript
680
+ // Pass custom context to the spawned instance
681
+ const myContext = { userId: 123, permissions: ['read', 'write'] };
682
+ const instance = element.enh.get(registryItem, myContext);
683
+
684
+ // The constructor receives it via ctx.mountCtx
685
+ class MyEnhancement {
686
+ constructor(oElement, ctx, initVals) {
687
+ console.log(ctx.mountCtx.userId); // 123
688
+ console.log(ctx.mountCtx.permissions); // ['read', 'write']
689
+ }
690
+ }
691
+ ```
692
+
693
+ This is useful for:
694
+ - Passing authentication/authorization context
695
+ - Providing configuration that varies per invocation
696
+ - Sharing state between caller and enhancement
697
+ - Dependency injection of services or utilities
698
+
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).
700
+
701
+ </details>
702
+
648
703
  ### Registry Item with enhKey
649
704
 
650
- 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`:
651
706
 
652
707
  ```TypeScript
653
708
  interface IBaseRegistryItem<T> {
@@ -655,7 +710,7 @@ interface IBaseRegistryItem<T> {
655
710
  new (oElement?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
656
711
  canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
657
712
  };
658
- symlinks: { [key: string | symbol]: keyof T };
713
+ symlinks?: { [key: string | symbol]: keyof T };
659
714
  enhKey?: string; // String identifier for set proxy access
660
715
  withAttrs?: AttrPatterns<T>; // Automatic attribute parsing during spawn
661
716
  lifecycleKeys?:
@@ -669,11 +724,14 @@ interface IBaseRegistryItem<T> {
669
724
 
670
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.
671
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
+
672
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.
673
730
 
674
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.
675
732
 
676
- ### Advanced Examples
733
+ <details>
734
+ <summary>Advanced Examples</summary>
677
735
 
678
736
  **Multiple Enhancements:**
679
737
  ```TypeScript
@@ -696,8 +754,8 @@ const element = document.createElement('div');
696
754
  const registry = element.customElementRegistry.enhancementRegistry;
697
755
 
698
756
  registry.push([
699
- { spawn: StyleEnhancement, symlinks: {}, enhKey: 'styles' },
700
- { spawn: DataEnhancement, symlinks: {}, enhKey: 'data' }
757
+ { spawn: StyleEnhancement, enhKey: 'styles' },
758
+ { spawn: DataEnhancement, enhKey: 'data' }
701
759
  ]);
702
760
 
703
761
  element.enh.set.styles.height = '100px';
@@ -714,7 +772,6 @@ const registry = element.customElementRegistry.enhancementRegistry;
714
772
 
715
773
  registry.push({
716
774
  spawn: MyEnhancement,
717
- symlinks: {},
718
775
  enhKey: 'config'
719
776
  });
720
777
 
@@ -740,7 +797,10 @@ element.enh.set.plainData.prop2 = 'value2';
740
797
  console.log(element.enh.plainData); // { prop1: 'value1', prop2: 'value2' }
741
798
  ```
742
799
 
743
- ### Finding Registry Items by enhKey
800
+ </details>
801
+
802
+ <details>
803
+ <summary>Finding Registry Items by enhKey</summary>
744
804
 
745
805
  The `BaseRegistry` class includes a `findByEnhKey` method:
746
806
 
@@ -748,22 +808,21 @@ The `BaseRegistry` class includes a `findByEnhKey` method:
748
808
  const registry = new BaseRegistry();
749
809
  registry.push({
750
810
  spawn: MyEnhancement,
751
- symlinks: {},
752
811
  enhKey: 'myEnh'
753
812
  });
754
813
 
755
814
  const item = registry.findByEnhKey('myEnh');
756
815
  console.log(item.enhKey); // 'myEnh'
757
816
  ```
817
+ </details>
758
818
 
759
819
  ### Programmatic Instance Spawning with `enh.get()`
760
820
 
761
- 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:
762
822
 
763
823
  ```TypeScript
764
824
  const registryItem = {
765
825
  spawn: MyEnhancement,
766
- symlinks: {},
767
826
  enhKey: 'myEnh'
768
827
  };
769
828
 
@@ -786,7 +845,9 @@ console.log(element.enh.myEnh === instance); // true
786
845
  - **Shared instances**: Uses the same global instance map as `assignGingerly` and `enh.set`, ensuring only one instance per registry item
787
846
  - **Auto-registration**: Automatically adds registry items to the element's registry if not present
788
847
 
789
- **Example with shared instances:**
848
+ <details>
849
+ <summary>Example with shared instances</summary>
850
+
790
851
  ```TypeScript
791
852
  const registryItem = {
792
853
  spawn: MyEnhancement,
@@ -809,7 +870,10 @@ console.log(element.enh.myEnh.prop2); // 'from set'
809
870
  console.log(element.enh.myEnh.value); // 'from assign'
810
871
  ```
811
872
 
812
- ### Lifecycle Keys: Configuration vs Convention
873
+ </details>
874
+
875
+ <details>
876
+ <summary>Lifecycle Keys: Configuration vs Convention</summary>
813
877
 
814
878
  Enhancement classes can integrate with the lifecycle system through configurable method/property names, avoiding the need for base classes or mixins.
815
879
 
@@ -829,7 +893,6 @@ For convenience, you can use `lifecycleKeys: true` to adopt standard naming conv
829
893
  ```TypeScript
830
894
  const registryItem = {
831
895
  spawn: MyEnhancement,
832
- symlinks: {},
833
896
  enhKey: 'myEnh',
834
897
  lifecycleKeys: true // Uses standard names: "dispose" and "resolved"
835
898
  };
@@ -873,7 +936,6 @@ class MyEnhancement {
873
936
 
874
937
  const registryItem = {
875
938
  spawn: MyEnhancement,
876
- symlinks: {},
877
939
  enhKey: 'myEnh',
878
940
  lifecycleKeys: {
879
941
  dispose: DISPOSE,
@@ -884,6 +946,8 @@ const registryItem = {
884
946
 
885
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.
886
948
 
949
+ </details>
950
+
887
951
  ### Disposing Enhancement Instances with `enh.dispose()`
888
952
 
889
953
  The `enh.dispose()` method provides a way to clean up and remove enhancement instances:
@@ -907,7 +971,6 @@ class MyEnhancement {
907
971
 
908
972
  const registryItem = {
909
973
  spawn: MyEnhancement,
910
- symlinks: {},
911
974
  enhKey: 'myEnh',
912
975
  lifecycleKeys: true // Standard: calls dispose() method
913
976
  };
@@ -915,7 +978,6 @@ const registryItem = {
915
978
  // Or with custom name:
916
979
  const customRegistryItem = {
917
980
  spawn: MyEnhancement,
918
- symlinks: {},
919
981
  enhKey: 'myEnh',
920
982
  lifecycleKeys: {
921
983
  dispose: 'cleanup' // Custom: calls cleanup() method
@@ -966,7 +1028,6 @@ class TimerEnhancement {
966
1028
 
967
1029
  const registryItem = {
968
1030
  spawn: TimerEnhancement,
969
- symlinks: {},
970
1031
  enhKey: 'timer',
971
1032
  lifecycleKeys: true // Standard: calls dispose() method
972
1033
  };
@@ -1013,7 +1074,6 @@ class AsyncEnhancement extends EventTarget {
1013
1074
 
1014
1075
  const registryItem = {
1015
1076
  spawn: AsyncEnhancement,
1016
- symlinks: {},
1017
1077
  enhKey: 'asyncEnh',
1018
1078
  lifecycleKeys: true // Standard: watches "resolved" property and event
1019
1079
  };
@@ -1021,7 +1081,6 @@ const registryItem = {
1021
1081
  // Or with custom name:
1022
1082
  const customRegistryItem = {
1023
1083
  spawn: AsyncEnhancement,
1024
- symlinks: {},
1025
1084
  enhKey: 'asyncEnh',
1026
1085
  lifecycleKeys: {
1027
1086
  resolved: 'isReady' // Custom: watches "isReady" property and event
@@ -1031,9 +1090,15 @@ const customRegistryItem = {
1031
1090
  // Wait for the enhancement to be fully initialized
1032
1091
  const instance = await element.enh.whenResolved(registryItem);
1033
1092
  console.log(instance.data); // Data is loaded and ready
1093
+
1094
+ // With custom context
1095
+ const authContext = { token: 'abc123', userId: 456 };
1096
+ const instanceWithContext = await element.enh.whenResolved(registryItem, authContext);
1097
+ // The constructor receives authContext via ctx.mountCtx
1034
1098
  ```
1035
1099
 
1036
- **How `enh.whenResolved()` works:**
1100
+ <details>
1101
+ <summary>How `enh.whenResolved()` works:</summary>
1037
1102
 
1038
1103
  1. **Validates configuration**: Throws error if `lifecycleKeys.resolved` is not specified
1039
1104
  2. **Gets instance**: Calls `enh.get()` to get or spawn the instance
@@ -1094,7 +1159,6 @@ class DataEnhancement extends EventTarget {
1094
1159
 
1095
1160
  const registryItem = {
1096
1161
  spawn: DataEnhancement,
1097
- symlinks: {},
1098
1162
  enhKey: 'data',
1099
1163
  lifecycleKeys: true // Standard: watches "resolved" property and event
1100
1164
  };
@@ -1121,6 +1185,8 @@ console.log(instance1 === instance2); // true - same instance
1121
1185
 
1122
1186
  **Browser Support**: This feature requires Chrome 146+ with scoped custom element registry support.
1123
1187
 
1188
+ </details>
1189
+
1124
1190
  ## Conditional Spawning with `canSpawn`
1125
1191
 
1126
1192
  Enhancement classes can implement a static `canSpawn` method to conditionally block spawning based on the target object. This is useful for:
@@ -1154,7 +1220,6 @@ class DivOnlyEnhancement {
1154
1220
  const registry = new BaseRegistry();
1155
1221
  registry.push({
1156
1222
  spawn: DivOnlyEnhancement,
1157
- symlinks: {},
1158
1223
  enhKey: 'divOnly'
1159
1224
  });
1160
1225
 
@@ -1170,7 +1235,8 @@ const spanInstance = span.enh.get(registry.getItems()[0]);
1170
1235
  console.log(spanInstance); // undefined
1171
1236
  ```
1172
1237
 
1173
- ### How It Works
1238
+ <details>
1239
+ <summary>How It Works</summary>
1174
1240
 
1175
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
1176
1242
  2. **Receives context**: The method receives the target object and spawn context with registry item information
@@ -1281,6 +1347,8 @@ assignGingerly(element, { [enhSymbol]: 'test' }, { registry });
1281
1347
  // Enhancement created and value set
1282
1348
  ```
1283
1349
 
1350
+ </details>
1351
+
1284
1352
  ## Parsing Attributes with `parseWithAttrs`
1285
1353
 
1286
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.
@@ -1289,19 +1357,21 @@ The `parseWithAttrs` function provides a declarative way to read and parse HTML
1289
1357
 
1290
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.
1291
1359
 
1360
+ ```html
1361
+ <my-element my-enhancement-count="42" my-enhancement-theme="dark"></my-element>
1362
+ ```
1363
+
1292
1364
  ```TypeScript
1293
1365
  import 'assign-gingerly/object-extension.js';
1294
1366
 
1295
- // HTML: <my-element data-count="42" data-theme="dark"></my-element>
1296
-
1297
1367
  class MyEnhancement {
1298
- element;
1368
+ elementRef;
1299
1369
  ctx;
1300
1370
  count = 0;
1301
1371
  theme = 'light';
1302
1372
 
1303
1373
  constructor(oElement, ctx, initVals) {
1304
- this.element = oElement;
1374
+ this.element = new WeakRef(oElement);
1305
1375
  this.ctx = ctx;
1306
1376
  // initVals automatically contains parsed attributes!
1307
1377
  if (initVals) {
@@ -1311,23 +1381,21 @@ class MyEnhancement {
1311
1381
  }
1312
1382
 
1313
1383
  const element = document.querySelector('my-element');
1314
- const registry = element.customElementRegistry.enhancementRegistry;
1315
-
1316
- // Define withAttrs in the registry item
1317
- registry.push({
1384
+ const enhancementConfig = {
1318
1385
  spawn: MyEnhancement,
1319
- symlinks: {},
1320
1386
  enhKey: 'myEnh',
1321
1387
  withAttrs: {
1322
- base: 'data-',
1323
- count: '${base}count',
1388
+ base: 'my-enhancement',
1389
+ count: '${base}-count',
1324
1390
  _count: { instanceOf: 'Number' },
1325
- theme: '${base}theme'
1391
+ theme: '${base}-theme'
1392
+
1326
1393
  }
1327
- });
1394
+ };
1395
+
1328
1396
 
1329
1397
  // Spawn the enhancement - attributes are automatically parsed!
1330
- const instance = element.enh.get(registry.getItems()[0]);
1398
+ const instance = element.enh.get(enhancementConfig);
1331
1399
  console.log(instance.count); // 42 (parsed from attribute)
1332
1400
  console.log(instance.theme); // 'dark' (parsed from attribute)
1333
1401
  ```
@@ -1341,9 +1409,7 @@ console.log(instance.theme); // 'dark' (parsed from attribute)
1341
1409
 
1342
1410
  **Precedence**: If both parsed attributes and existing `element.enh[enhKey]` values exist, the existing values take precedence over parsed attributes.
1343
1411
 
1344
- ### Manual Usage
1345
1412
 
1346
- While automatic parsing is the recommended approach, you can also call `parseWithAttrs()` manually when needed:
1347
1413
 
1348
1414
  ### The `enh-` Prefix for Attribute Isolation
1349
1415
 
@@ -1379,7 +1445,6 @@ For custom elements and SVG, you can opt-in to reading unprefixed attributes by
1379
1445
  // Allow unprefixed for elements matching pattern
1380
1446
  registry.push({
1381
1447
  spawn: MyEnhancement,
1382
- symlinks: {},
1383
1448
  enhKey: 'myEnh',
1384
1449
  allowUnprefixed: '^my-', // Only for elements starting with "my-"
1385
1450
  withAttrs: {
@@ -1392,7 +1457,6 @@ registry.push({
1392
1457
  // Or use RegExp for more complex patterns
1393
1458
  registry.push({
1394
1459
  spawn: MyEnhancement,
1395
- symlinks: {},
1396
1460
  enhKey: 'myEnh',
1397
1461
  allowUnprefixed: /^(my-|app-)/, // For "my-*" or "app-*" elements
1398
1462
  withAttrs: {
@@ -1403,7 +1467,23 @@ registry.push({
1403
1467
  });
1404
1468
  ```
1405
1469
 
1406
- 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:
1407
1487
 
1408
1488
  ```TypeScript
1409
1489
  // Allow unprefixed only for elements matching pattern
@@ -1442,27 +1522,6 @@ const result2 = parseWithAttrs(
1442
1522
  // result2.count = undefined (unprefixed ignored because tag doesn't match)
1443
1523
  ```
1444
1524
 
1445
- **Base Attribute Validation:**
1446
-
1447
- The `base` attribute must contain either a dash (`-`) or a non-ASCII character to prevent conflicts with native attributes:
1448
-
1449
- ```TypeScript
1450
- // Valid base attributes
1451
- parseWithAttrs(element, { base: 'data-config' }); // Has dash
1452
- parseWithAttrs(element, { base: '🎨-theme' }); // Has non-ASCII
1453
-
1454
- // Invalid - throws error
1455
- parseWithAttrs(element, { base: 'config' }); // No dash or non-ASCII
1456
- ```
1457
-
1458
- **Why use `enh-` prefix?**
1459
-
1460
- 1. **Avoid conflicts**: Custom elements may use unprefixed attributes for their own purposes
1461
- 2. **Clear intent**: Makes it obvious which attributes are for enhancements
1462
- 3. **Future-proof**: Protects against future attribute additions to custom elements
1463
- 4. **Consistency**: Provides a standard convention across all enhanced elements
1464
- 5. **Selective override**: Pattern-based `allowUnprefixed` lets you opt-in specific element families while maintaining strict isolation for others
1465
-
1466
1525
  ### Basic Usage
1467
1526
 
1468
1527
  ```TypeScript
@@ -1479,7 +1538,59 @@ const config = parseWithAttrs(element, {
1479
1538
  });
1480
1539
  ```
1481
1540
 
1482
- ### 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>
1483
1594
 
1484
1595
  The `parseWithAttrs` function accepts an `AttrPatterns` object that defines:
1485
1596
 
@@ -1564,6 +1675,139 @@ const result = parseWithAttrs(element, {
1564
1675
  // Result: { createdAt: 1705315800000 }
1565
1676
  ```
1566
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
+
1567
1811
  ### Property Mapping with mapsTo
1568
1812
 
1569
1813
  The `mapsTo` property controls where parsed values are placed:
@@ -1597,6 +1841,250 @@ const result = parseWithAttrs(element, {
1597
1841
  // Result: { theme: 'dark', lang: 'en' }
1598
1842
  ```
1599
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
+
1600
2088
  ### Base Attribute
1601
2089
 
1602
2090
  The special `base` property handles a single attribute that spreads into the result:
@@ -1621,6 +2109,15 @@ const result2 = parseWithAttrs(element, {
1621
2109
  // Result: { greetings: { hello: 'world', goodbye: 'Mars' } }
1622
2110
  ```
1623
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
+
1624
2121
  ### Nested Paths with assignGingerly
1625
2122
 
1626
2123
  Combine `parseWithAttrs` with `assignGingerly` for nested property assignment:
@@ -1647,6 +2144,10 @@ assignGingerly(element, attrs);
1647
2144
  // element.moods.personIsHappy === true
1648
2145
  ```
1649
2146
 
2147
+ </details>
2148
+
2149
+ <!--
2150
+
1650
2151
  ### Complete Example
1651
2152
 
1652
2153
  ```TypeScript
@@ -1686,46 +2187,8 @@ console.log(result);
1686
2187
  // }
1687
2188
  ```
1688
2189
 
1689
- ### Error Handling
2190
+ -->
1690
2191
 
1691
- The function throws descriptive errors for common issues:
1692
2192
 
1693
- ```TypeScript
1694
- // Circular reference
1695
- parseWithAttrs(element, {
1696
- a: '${b}',
1697
- b: '${a}' // Error: Circular reference detected
1698
- });
1699
2193
 
1700
- // Undefined variable
1701
- parseWithAttrs(element, {
1702
- name: '${missing}' // Error: Undefined template variable: missing
1703
- });
1704
2194
 
1705
- // Invalid JSON
1706
- // HTML: <div data-obj='{invalid}'></div>
1707
- parseWithAttrs(element, {
1708
- base: 'data-',
1709
- obj: '${base}obj',
1710
- _obj: { instanceOf: 'Object' }
1711
- // Error: Failed to parse JSON: "{invalid}"
1712
- });
1713
-
1714
- // Invalid number
1715
- // HTML: <div data-count="abc"></div>
1716
- parseWithAttrs(element, {
1717
- base: 'data-',
1718
- count: '${base}count',
1719
- _count: { instanceOf: 'Number' }
1720
- // Error: Failed to parse number: "abc"
1721
- });
1722
- ```
1723
-
1724
- ### Best Practices
1725
-
1726
- 1. **Use base for common prefixes**: Reduces repetition in attribute names
1727
- 2. **Leverage template variables**: Build complex attribute names from simple parts
1728
- 3. **Specify instanceOf**: Ensures proper type conversion
1729
- 4. **Use mapsTo for clarity**: Map attribute names to meaningful property names
1730
- 5. **Combine with assignGingerly**: Use nested paths (`?.`) for deep property assignment
1731
- 6. **Handle missing attributes**: Non-existent attributes are skipped (except Boolean types)