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 +547 -117
- package/index.js +2 -0
- package/index.ts +2 -0
- package/package.json +8 -1
- package/parseWithAttrs.js +150 -6
- package/parseWithAttrs.ts +176 -6
- package/parserRegistry.js +71 -0
- package/parserRegistry.ts +94 -0
- package/types.d.ts +22 -2
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
728
|
-
{ spawn: DataEnhancement,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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(
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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)
|