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 +581 -118
- package/assignGingerly.js +10 -6
- package/assignGingerly.ts +14 -25
- package/index.js +2 -0
- package/index.ts +2 -0
- package/object-extension.js +9 -7
- package/object-extension.ts +9 -7
- 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 +24 -3
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
700
|
-
{ spawn: DataEnhancement,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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(
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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)
|