assign-gingerly 0.0.30 → 0.0.32
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 +415 -346
- package/assignFrom.js +27 -0
- package/assignFrom.ts +37 -0
- package/assignGingerly.js +221 -5
- package/assignGingerly.ts +287 -5
- package/eachTime.js +110 -0
- package/eachTime.ts +137 -0
- package/index.js +2 -0
- package/index.ts +2 -0
- package/object-extension.js +65 -12
- package/object-extension.ts +74 -15
- package/package.json +10 -6
- package/playwright.config.ts +1 -1
- package/resolveValues.js +44 -0
- package/resolveValues.ts +45 -0
- package/types/assign-gingerly/types.d.ts +12 -2
- package/Infer.js +0 -154
- package/Infer.ts +0 -190
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# assign-gingerly and assign-tentatively
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
|
|
4
4
|
[](http://badge.fury.io/js/assign-gingerly)
|
|
@@ -411,25 +411,31 @@ assignGingerly(elementRef, {
|
|
|
411
411
|
// Equivalent to: elementRef.deref().classList.add('active')
|
|
412
412
|
```
|
|
413
413
|
|
|
414
|
-
**Complex chaining:**
|
|
414
|
+
**Complex chaining with real DOM elements:**
|
|
415
|
+
|
|
416
|
+
Methods are called on the objects found through chained accessors, not just on the root object:
|
|
415
417
|
|
|
416
418
|
```TypeScript
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
};
|
|
419
|
+
const div = document.createElement('div');
|
|
420
|
+
div.innerHTML = `
|
|
421
|
+
<my-element>
|
|
422
|
+
<your-element></your-element>
|
|
423
|
+
</my-element>
|
|
424
|
+
`;
|
|
425
425
|
|
|
426
|
-
assignGingerly(
|
|
427
|
-
'?.querySelector?.my-element?.classList?.add': 'highlighted'
|
|
426
|
+
assignGingerly(div, {
|
|
427
|
+
'?.querySelector?.my-element?.querySelector?.your-element?.classList?.add': 'highlighted'
|
|
428
428
|
}, { withMethods: ['querySelector', 'add'] });
|
|
429
429
|
|
|
430
|
-
// Equivalent to:
|
|
430
|
+
// Equivalent to:
|
|
431
|
+
// div.querySelector('my-element').querySelector('your-element').classList.add('highlighted')
|
|
432
|
+
|
|
433
|
+
const yourElement = div.querySelector('my-element')?.querySelector('your-element');
|
|
434
|
+
console.log(yourElement?.classList.contains('highlighted')); // true
|
|
431
435
|
```
|
|
432
436
|
|
|
437
|
+
The key insight: `querySelector` is called on each intermediate result in the chain. First on `div`, then on the `my-element` result, demonstrating that methods work naturally with the object hierarchy you're navigating.
|
|
438
|
+
|
|
433
439
|
**Using Set for withMethods:**
|
|
434
440
|
|
|
435
441
|
For better performance with many methods, use a Set:
|
|
@@ -464,6 +470,321 @@ assignGingerly(element, {
|
|
|
464
470
|
- Silent failure for non-existent methods (garbage in, garbage out)
|
|
465
471
|
- Supports method chaining and complex navigation patterns
|
|
466
472
|
|
|
473
|
+
## Example 3d - Aliasing with aka
|
|
474
|
+
|
|
475
|
+
The `aka` option allows you to define custom shortcuts (aliases) for property and method names, reducing verbosity in repetitive patterns. This is inspired by jQuery's `$` shortcut for `querySelectorAll`, but fully customizable.
|
|
476
|
+
|
|
477
|
+
```TypeScript
|
|
478
|
+
import assignGingerly from 'assign-gingerly';
|
|
479
|
+
|
|
480
|
+
const div = document.createElement('div');
|
|
481
|
+
div.innerHTML = `
|
|
482
|
+
<my-element>
|
|
483
|
+
<your-element></your-element>
|
|
484
|
+
</my-element>
|
|
485
|
+
`;
|
|
486
|
+
|
|
487
|
+
// Without aliases (verbose)
|
|
488
|
+
assignGingerly(div, {
|
|
489
|
+
'?.querySelector?.my-element?.classList?.add': 'highlighted',
|
|
490
|
+
'?.querySelector?.your-element?.classList?.add': 'active'
|
|
491
|
+
}, { withMethods: ['querySelector', 'add'] });
|
|
492
|
+
|
|
493
|
+
// With aliases (concise)
|
|
494
|
+
assignGingerly(div, {
|
|
495
|
+
'?.$?.my-element?.c?.+': 'highlighted',
|
|
496
|
+
'?.$?.your-element?.c?.+': 'active'
|
|
497
|
+
}, {
|
|
498
|
+
withMethods: ['querySelector', 'add'],
|
|
499
|
+
aka: { '$': 'querySelector', 'c': 'classList', '+': 'add' }
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**How it works:**
|
|
504
|
+
|
|
505
|
+
- Aliases are substituted **before** path evaluation
|
|
506
|
+
- Matches complete tokens between `?.` delimiters (not substrings)
|
|
507
|
+
- Works for both properties and methods
|
|
508
|
+
- Single or multi-character aliases supported
|
|
509
|
+
|
|
510
|
+
**Reserved characters:**
|
|
511
|
+
|
|
512
|
+
Cannot be used in aliases: space (` `), backtick (`` ` ``)
|
|
513
|
+
|
|
514
|
+
**Multi-character aliases:**
|
|
515
|
+
|
|
516
|
+
```TypeScript
|
|
517
|
+
assignGingerly(element, {
|
|
518
|
+
'?.qs?.my-element?.cl?.add': 'highlighted'
|
|
519
|
+
}, {
|
|
520
|
+
withMethods: ['querySelector', 'add'],
|
|
521
|
+
aka: { 'qs': 'querySelector', 'cl': 'classList' }
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Multiple aliases in one path:**
|
|
526
|
+
|
|
527
|
+
```TypeScript
|
|
528
|
+
assignGingerly(element, {
|
|
529
|
+
'?.c?.+': 'class1',
|
|
530
|
+
'?.p?.+': 'part1',
|
|
531
|
+
'?.ds?.userId': '123'
|
|
532
|
+
}, {
|
|
533
|
+
withMethods: ['add'],
|
|
534
|
+
aka: {
|
|
535
|
+
'c': 'classList',
|
|
536
|
+
'p': 'part',
|
|
537
|
+
'ds': 'dataset',
|
|
538
|
+
'+': 'add'
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Equivalent to:
|
|
543
|
+
// element.classList.add('class1')
|
|
544
|
+
// element.part.add('part1')
|
|
545
|
+
// element.dataset.userId = '123'
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Benefits:**
|
|
549
|
+
|
|
550
|
+
- Reduces verbosity in repetitive patterns
|
|
551
|
+
- Fully customizable shortcuts
|
|
552
|
+
- Improves readability when you have many similar operations
|
|
553
|
+
- Works seamlessly with `withMethods`
|
|
554
|
+
|
|
555
|
+
## Example 3e - ForEach with @each
|
|
556
|
+
|
|
557
|
+
The `@each` symbol allows you to iterate over collections and apply operations to each item. This works with any iterable including Arrays, NodeList, HTMLCollection, and more.
|
|
558
|
+
|
|
559
|
+
```TypeScript
|
|
560
|
+
import assignGingerly from 'assign-gingerly';
|
|
561
|
+
|
|
562
|
+
const div = document.createElement('div');
|
|
563
|
+
div.innerHTML = `
|
|
564
|
+
<my-element></my-element>
|
|
565
|
+
<my-element></my-element>
|
|
566
|
+
<my-element></my-element>
|
|
567
|
+
`;
|
|
568
|
+
|
|
569
|
+
// Apply to each element in the collection
|
|
570
|
+
assignGingerly(div, {
|
|
571
|
+
'?.querySelectorAll?.my-element?.@each?.classList?.add': 'highlighted'
|
|
572
|
+
}, { withMethods: ['querySelectorAll', 'add'] });
|
|
573
|
+
|
|
574
|
+
// All my-element elements now have the 'highlighted' class
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**How it works:**
|
|
578
|
+
|
|
579
|
+
- `@each` marks the point where iteration begins
|
|
580
|
+
- Everything before `@each` navigates to the iterable
|
|
581
|
+
- Everything after `@each` is applied to each item in the collection
|
|
582
|
+
- Empty collections are handled gracefully (no errors)
|
|
583
|
+
|
|
584
|
+
**With regular arrays:**
|
|
585
|
+
|
|
586
|
+
```TypeScript
|
|
587
|
+
const obj = {
|
|
588
|
+
items: [
|
|
589
|
+
{ value: null },
|
|
590
|
+
{ value: null },
|
|
591
|
+
{ value: null }
|
|
592
|
+
]
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
assignGingerly(obj, {
|
|
596
|
+
'?.items?.@each?.value': 'test'
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// All items now have value: 'test'
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**Nested forEach:**
|
|
603
|
+
|
|
604
|
+
```TypeScript
|
|
605
|
+
const obj = {
|
|
606
|
+
groups: [
|
|
607
|
+
{ items: [{ value: null }, { value: null }] },
|
|
608
|
+
{ items: [{ value: null }, { value: null }] }
|
|
609
|
+
]
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
assignGingerly(obj, {
|
|
613
|
+
'?.groups?.@each?.items?.@each?.value': 'nested'
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// All nested items now have value: 'nested'
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**With aliases:**
|
|
620
|
+
|
|
621
|
+
```TypeScript
|
|
622
|
+
assignGingerly(div, {
|
|
623
|
+
'?.qsa?.my-element?.*?.c?.+': 'highlighted'
|
|
624
|
+
}, {
|
|
625
|
+
withMethods: ['querySelectorAll', 'add'],
|
|
626
|
+
aka: {
|
|
627
|
+
'qsa': 'querySelectorAll',
|
|
628
|
+
'c': 'classList',
|
|
629
|
+
'+': 'add',
|
|
630
|
+
'*': '@each' // Alias * to @each for brevity
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Method calls on each item:**
|
|
636
|
+
|
|
637
|
+
```TypeScript
|
|
638
|
+
assignGingerly(div, {
|
|
639
|
+
'?.querySelectorAll?.div?.@each?.setAttribute': ['data-id', '123']
|
|
640
|
+
}, { withMethods: ['querySelectorAll', 'setAttribute'] });
|
|
641
|
+
|
|
642
|
+
// All div elements now have data-id="123"
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Accessing iterable properties:**
|
|
646
|
+
|
|
647
|
+
When you omit `@each`, you access properties on the iterable itself, not its items:
|
|
648
|
+
|
|
649
|
+
```TypeScript
|
|
650
|
+
const obj = {
|
|
651
|
+
items: [1, 2, 3],
|
|
652
|
+
customProp: null
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Set property on the array itself
|
|
656
|
+
assignGingerly(obj, {
|
|
657
|
+
'?.items?.customProp': 'test'
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
console.log(obj.items.customProp); // 'test'
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Benefits:**
|
|
664
|
+
|
|
665
|
+
- Works with any iterable (Arrays, NodeList, HTMLCollection, etc.)
|
|
666
|
+
- Supports nested iterations
|
|
667
|
+
- Integrates seamlessly with `withMethods` and `aka`
|
|
668
|
+
- Clear distinction between iterating and accessing iterable properties
|
|
669
|
+
- Graceful handling of empty collections
|
|
670
|
+
|
|
671
|
+
## Example 3f - Reactive Iteration with @eachTime
|
|
672
|
+
|
|
673
|
+
The `@eachTime` symbol enables reactive iteration over elements as they mount or appear dynamically. Unlike `@each` which operates on static collections, `@eachTime` subscribes to events and applies operations to elements as they arrive over time.
|
|
674
|
+
|
|
675
|
+
**Important:** This feature requires an `AbortSignal` for cleanup and is designed to work with EventTarget objects that emit 'mount' events (such as [mount-observer](https://github.com/bahrus/mount-observer)).
|
|
676
|
+
|
|
677
|
+
```TypeScript
|
|
678
|
+
import assignGingerly from 'assign-gingerly';
|
|
679
|
+
|
|
680
|
+
const controller = new AbortController();
|
|
681
|
+
const div = document.createElement('div');
|
|
682
|
+
|
|
683
|
+
// Assume mountObserver is an IMountObserver instance that emits 'mount' events
|
|
684
|
+
// when new elements matching 'my-element' are added to the DOM
|
|
685
|
+
|
|
686
|
+
assignGingerly(div, {
|
|
687
|
+
'?.mountObserver?.@eachTime?.classList?.add': 'highlighted'
|
|
688
|
+
}, {
|
|
689
|
+
withMethods: ['add'],
|
|
690
|
+
signal: controller.signal // Required for cleanup
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// As elements mount, they automatically get the 'highlighted' class
|
|
694
|
+
// Later, cleanup all listeners:
|
|
695
|
+
controller.abort();
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**How it works:**
|
|
699
|
+
|
|
700
|
+
- `@eachTime` marks the point where reactive iteration begins
|
|
701
|
+
- Everything before `@eachTime` must navigate to an EventTarget
|
|
702
|
+
- The EventTarget must emit 'mount' events with a `mountedElement` property
|
|
703
|
+
- Everything after `@eachTime` is applied to each mounted element
|
|
704
|
+
- Event listeners are automatically cleaned up when the AbortSignal is aborted
|
|
705
|
+
|
|
706
|
+
**With method calls:**
|
|
707
|
+
|
|
708
|
+
```TypeScript
|
|
709
|
+
const controller = new AbortController();
|
|
710
|
+
|
|
711
|
+
assignGingerly(div, {
|
|
712
|
+
'?.mountObserver?.@eachTime?.setAttribute': ['data-mounted', 'true']
|
|
713
|
+
}, {
|
|
714
|
+
withMethods: ['setAttribute'],
|
|
715
|
+
signal: controller.signal
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Each mounted element gets data-mounted="true"
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**With aliases:**
|
|
722
|
+
|
|
723
|
+
```TypeScript
|
|
724
|
+
const controller = new AbortController();
|
|
725
|
+
|
|
726
|
+
assignGingerly(div, {
|
|
727
|
+
'?.mo?.@*?.c?.+': 'active'
|
|
728
|
+
}, {
|
|
729
|
+
withMethods: ['add'],
|
|
730
|
+
aka: {
|
|
731
|
+
'mo': 'mountObserver',
|
|
732
|
+
'@*': '@eachTime',
|
|
733
|
+
'c': 'classList',
|
|
734
|
+
'+': 'add'
|
|
735
|
+
},
|
|
736
|
+
signal: controller.signal
|
|
737
|
+
});
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Cleanup is required:**
|
|
741
|
+
|
|
742
|
+
```TypeScript
|
|
743
|
+
const controller = new AbortController();
|
|
744
|
+
|
|
745
|
+
// Setup reactive iteration
|
|
746
|
+
assignGingerly(div, {
|
|
747
|
+
'?.mountObserver?.@eachTime?.classList?.add': 'mounted'
|
|
748
|
+
}, {
|
|
749
|
+
withMethods: ['add'],
|
|
750
|
+
signal: controller.signal
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Later, when you're done observing:
|
|
754
|
+
controller.abort(); // Removes all event listeners
|
|
755
|
+
|
|
756
|
+
// Attempting to use @eachTime without a signal throws an error:
|
|
757
|
+
assignGingerly(div, {
|
|
758
|
+
'?.mountObserver?.@eachTime?.classList?.add': 'mounted'
|
|
759
|
+
}, { withMethods: ['add'] });
|
|
760
|
+
// Error: @eachTime requires an AbortSignal in options.signal for cleanup
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Key differences from @each:**
|
|
764
|
+
|
|
765
|
+
| Feature | @each | @eachTime |
|
|
766
|
+
|---------|-------|-----------|
|
|
767
|
+
| **Type** | Static iteration | Reactive iteration |
|
|
768
|
+
| **Timing** | Immediate (synchronous) | Over time (asynchronous) |
|
|
769
|
+
| **Use case** | Existing collections | Elements appearing dynamically |
|
|
770
|
+
| **Cleanup** | Not needed | Required (AbortSignal) |
|
|
771
|
+
| **Requirements** | Any iterable | EventTarget with 'mount' events |
|
|
772
|
+
|
|
773
|
+
**Benefits:**
|
|
774
|
+
|
|
775
|
+
- Declarative reactive programming without RxJS complexity
|
|
776
|
+
- Automatic cleanup via standard AbortSignal API
|
|
777
|
+
- JSON-serializable configuration (behavior is in implementation)
|
|
778
|
+
- Fire-and-forget async pattern (doesn't block)
|
|
779
|
+
- Minimal weight impact (~3% when not used, dynamically loaded when needed)
|
|
780
|
+
|
|
781
|
+
**Limitations:**
|
|
782
|
+
|
|
783
|
+
- Requires EventTarget that emits 'mount' events
|
|
784
|
+
- AbortSignal is mandatory for cleanup
|
|
785
|
+
- Testing is done in mount-observer package (no tests in assign-gingerly)
|
|
786
|
+
- Single @eachTime per path (nested @eachTime not currently supported)
|
|
787
|
+
|
|
467
788
|
While we are in the business of passing values of object A into object B, we might as well add some extremely common behavior that allows updating properties of object B based on the current values of object B -- things like incrementing, toggling, and deleting. Deleting is critical for assignTentatively, but is included with both functions.
|
|
468
789
|
|
|
469
790
|
## Example 4 - Incrementing values with += command
|
|
@@ -649,10 +970,12 @@ console.log(string1 === string2); // true
|
|
|
649
970
|
|
|
650
971
|
This guarantees that applying the reversal object restores the object to its exact original state.
|
|
651
972
|
|
|
973
|
+
# Object and Element Enhancements via assign-gingerly
|
|
974
|
+
|
|
652
975
|
## Dependency injection based on a registry object and a Symbolic reference mapping
|
|
653
976
|
|
|
654
977
|
```Typescript
|
|
655
|
-
interface
|
|
978
|
+
interface EnhancementConfig<T = any, TObjToExtend = any> {
|
|
656
979
|
spawn: {new(objToExtend: TObjToExtend, ctx: SpawnContext, initVals: Partial<T>): T}
|
|
657
980
|
symlinks?: {[key: symbol]: keyof T}
|
|
658
981
|
// Optional: for element enhancement access
|
|
@@ -678,7 +1001,7 @@ class YourEnhancement{
|
|
|
678
1001
|
}
|
|
679
1002
|
|
|
680
1003
|
class EnhancementRegistry{
|
|
681
|
-
push(
|
|
1004
|
+
push(EnhancementConfig | EnhancementConfig[]){
|
|
682
1005
|
...
|
|
683
1006
|
}
|
|
684
1007
|
}
|
|
@@ -1035,7 +1358,7 @@ Element enhancement classes should follow this constructor signature:
|
|
|
1035
1358
|
|
|
1036
1359
|
```TypeScript
|
|
1037
1360
|
interface SpawnContext<T, TMountContext = any> {
|
|
1038
|
-
config:
|
|
1361
|
+
config: EnhancementConfig<T>;
|
|
1039
1362
|
mountCtx?: TMountContext; // Optional custom context passed by caller
|
|
1040
1363
|
}
|
|
1041
1364
|
|
|
@@ -1089,7 +1412,7 @@ This is useful for:
|
|
|
1089
1412
|
In addition to spawn and symlinks, registry items support optional properties `enhKey`, `withAttrs`, `canSpawn`, and `lifecycleKeys`:
|
|
1090
1413
|
|
|
1091
1414
|
```TypeScript
|
|
1092
|
-
interface
|
|
1415
|
+
interface EnhancementConfig<T, TObj = Element> {
|
|
1093
1416
|
spawn: {
|
|
1094
1417
|
new (obj?: TObj, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
|
|
1095
1418
|
canSpawn?: (obj: TObj, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
|
|
@@ -1229,6 +1552,37 @@ console.log(element.enh.myEnh === instance); // true
|
|
|
1229
1552
|
- **Shared instances**: Uses the same global instance map as `assignGingerly` and `enh.set`, ensuring only one instance per registry item
|
|
1230
1553
|
- **Auto-registration**: Automatically adds registry items to the element's registry if not present
|
|
1231
1554
|
|
|
1555
|
+
**Lookup by enhKey (string or symbol):**
|
|
1556
|
+
|
|
1557
|
+
Instead of passing the full registry item object, you can pass a string or symbol matching the `enhKey` of a previously registered enhancement:
|
|
1558
|
+
|
|
1559
|
+
```TypeScript
|
|
1560
|
+
// First, register the enhancement (e.g., via mount-observer or manually)
|
|
1561
|
+
registry.push({
|
|
1562
|
+
spawn: MyEnhancement,
|
|
1563
|
+
enhKey: 'myEnh'
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// Later, retrieve by enhKey string
|
|
1567
|
+
const instance = element.enh.get('myEnh');
|
|
1568
|
+
|
|
1569
|
+
// Or by symbol enhKey
|
|
1570
|
+
const enhSym = Symbol.for('myEnh');
|
|
1571
|
+
const instance2 = element.enh.get(enhSym);
|
|
1572
|
+
```
|
|
1573
|
+
|
|
1574
|
+
If the enhKey is not found in the registry, an error is thrown: `"myEnh not in registry"`.
|
|
1575
|
+
|
|
1576
|
+
This also works with `enh.dispose()` and `enh.whenResolved()`:
|
|
1577
|
+
|
|
1578
|
+
```TypeScript
|
|
1579
|
+
// Dispose by enhKey
|
|
1580
|
+
element.enh.dispose('myEnh');
|
|
1581
|
+
|
|
1582
|
+
// Wait for resolution by enhKey
|
|
1583
|
+
const resolved = await element.enh.whenResolved('myEnh');
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1232
1586
|
<details>
|
|
1233
1587
|
<summary>Example with shared instances</summary>
|
|
1234
1588
|
|
|
@@ -1460,19 +1814,19 @@ element.remove();
|
|
|
1460
1814
|
|
|
1461
1815
|
// Case 1: Temporarily removed, will be re-added
|
|
1462
1816
|
setTimeout(() => document.body.append(element), 1000);
|
|
1463
|
-
//
|
|
1817
|
+
// ? Don't dispose - enhancement should persist
|
|
1464
1818
|
|
|
1465
1819
|
// Case 2: Moved to another location
|
|
1466
1820
|
otherContainer.append(element);
|
|
1467
|
-
//
|
|
1821
|
+
// ? Don't dispose - enhancement should persist
|
|
1468
1822
|
|
|
1469
1823
|
// Case 3: Cached for reuse
|
|
1470
1824
|
elementCache.set('myElement', element);
|
|
1471
|
-
//
|
|
1825
|
+
// ? Don't dispose - enhancement should persist
|
|
1472
1826
|
|
|
1473
1827
|
// Case 4: Truly done, ready for GC
|
|
1474
1828
|
element = null;
|
|
1475
|
-
//
|
|
1829
|
+
// ? Should dispose, but no way to detect this automatically
|
|
1476
1830
|
```
|
|
1477
1831
|
|
|
1478
1832
|
**Practical disposal strategies:**
|
|
@@ -1566,10 +1920,10 @@ class MyEnhancement {
|
|
|
1566
1920
|
```
|
|
1567
1921
|
|
|
1568
1922
|
**Summary:**
|
|
1569
|
-
-
|
|
1570
|
-
-
|
|
1571
|
-
-
|
|
1572
|
-
-
|
|
1923
|
+
- ? Storage mechanism prevents memory leaks via WeakMap
|
|
1924
|
+
- ?? Enhancement internals need manual cleanup via dispose()
|
|
1925
|
+
- ? No automatic way to detect when disposal should happen
|
|
1926
|
+
- ?? Choose disposal strategy based on your application's lifecycle
|
|
1573
1927
|
|
|
1574
1928
|
### Waiting for Async Initialization with `enh.whenResolved(regItem)`
|
|
1575
1929
|
|
|
@@ -1779,7 +2133,7 @@ static canSpawn(obj: any, ctx?: SpawnContext<T>): boolean
|
|
|
1779
2133
|
```
|
|
1780
2134
|
|
|
1781
2135
|
- `obj`: The target object being enhanced (element, plain object, etc.)
|
|
1782
|
-
- `ctx`: Optional spawn context containing `{ config:
|
|
2136
|
+
- `ctx`: Optional spawn context containing `{ config: EnhancementConfig<T> }`
|
|
1783
2137
|
- Returns: `true` to allow spawning, `false` to block
|
|
1784
2138
|
|
|
1785
2139
|
### Use Cases
|
|
@@ -1976,7 +2330,7 @@ console.log(instance.value); // 'test123' (parsed from attribute)
|
|
|
1976
2330
|
|
|
1977
2331
|
</details>
|
|
1978
2332
|
|
|
1979
|
-
> !
|
|
2333
|
+
> [!NOTE]
|
|
1980
2334
|
> `withAttrs` works with or without `enhKey`. When there's no `enhKey`, the parsed attributes are passed directly to the constructor. When there is an `enhKey`, they're merged with any pre-existing values on the enh container.
|
|
1981
2335
|
|
|
1982
2336
|
### The `enh-` Prefix for Attribute Isolation
|
|
@@ -2150,7 +2504,7 @@ The `base` attribute must contain either a dash (`-`) or a non-ASCII character t
|
|
|
2150
2504
|
```TypeScript
|
|
2151
2505
|
// Valid base attributes
|
|
2152
2506
|
const enhConfig1 = { base: 'data-config' }; // Has dash
|
|
2153
|
-
const enhConfig2 = { base: '
|
|
2507
|
+
const enhConfig2 = { base: '??-theme' }); // Has non-ASCII (and dash)
|
|
2154
2508
|
|
|
2155
2509
|
// Invalid - throws error
|
|
2156
2510
|
const enhConig3 = { base: 'config' }; // No dash or non-ASCII
|
|
@@ -2198,6 +2552,33 @@ const result = parseWithAttrs(element, {
|
|
|
2198
2552
|
// Result: { name: 'Alice', age: '30' }
|
|
2199
2553
|
```
|
|
2200
2554
|
|
|
2555
|
+
**Deep Nesting:**
|
|
2556
|
+
|
|
2557
|
+
Template variables can reference other template variables to any depth, creating hierarchical attribute naming patterns:
|
|
2558
|
+
|
|
2559
|
+
```TypeScript
|
|
2560
|
+
// HTML: <div data-app-user-profile-name="Alice" data-app-user-profile-email="alice@example.com"></div>
|
|
2561
|
+
|
|
2562
|
+
const result = parseWithAttrs(element, {
|
|
2563
|
+
base: 'data-',
|
|
2564
|
+
app: '${base}app',
|
|
2565
|
+
user: '${app}-user',
|
|
2566
|
+
profile: '${user}-profile',
|
|
2567
|
+
name: '${profile}-name',
|
|
2568
|
+
email: '${profile}-email'
|
|
2569
|
+
});
|
|
2570
|
+
// Result: { name: 'Alice', email: 'alice@example.com' }
|
|
2571
|
+
|
|
2572
|
+
// The resolution chain: base ? app ? user ? profile ? name/email
|
|
2573
|
+
// Resolves to: data-app-user-profile-name and data-app-user-profile-email
|
|
2574
|
+
```
|
|
2575
|
+
|
|
2576
|
+
**Benefits of hierarchical variables:**
|
|
2577
|
+
- Build complex attribute names from simple parts
|
|
2578
|
+
- Maintain consistency across related attributes
|
|
2579
|
+
- Easy to refactor by changing a single variable
|
|
2580
|
+
- Self-documenting attribute structure
|
|
2581
|
+
|
|
2201
2582
|
Template variables are resolved recursively and cached for performance. Circular references are detected and throw an error.
|
|
2202
2583
|
|
|
2203
2584
|
### Type Parsing with instanceOf
|
|
@@ -2376,12 +2757,12 @@ registerCommonParsers(globalParserRegistry);
|
|
|
2376
2757
|
|
|
2377
2758
|
**Benefits of Named Parsers:**
|
|
2378
2759
|
|
|
2379
|
-
-
|
|
2380
|
-
-
|
|
2381
|
-
-
|
|
2382
|
-
-
|
|
2383
|
-
-
|
|
2384
|
-
-
|
|
2760
|
+
- ? **JSON serializable** - Configs can be stored/transmitted as JSON
|
|
2761
|
+
- ? **Reusable** - Define once, use everywhere
|
|
2762
|
+
- ? **Maintainable** - Update parser logic in one place
|
|
2763
|
+
- ? **Testable** - Test parsers independently
|
|
2764
|
+
- ? **Discoverable** - `globalParserRegistry.getNames()` lists all available parsers
|
|
2765
|
+
- ? **Backward compatible** - Inline functions still work
|
|
2385
2766
|
|
|
2386
2767
|
**Mixing Inline and Named Parsers:**
|
|
2387
2768
|
|
|
@@ -3408,315 +3789,3 @@ ItemScope Managers follow these design principles:
|
|
|
3408
3789
|
|
|
3409
3790
|
This design ensures backward compatibility while providing powerful new capabilities for managing DOM fragments.
|
|
3410
3791
|
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
## Smart Value Assignment with Infer Enhancement
|
|
3415
|
-
|
|
3416
|
-
The Infer enhancement provides a symbol-based API for smart value and display property assignment. Instead of manually determining which property to set on different element types (e.g., `value` for inputs, `checked` for checkboxes, `textContent` for divs), the Infer enhancement automatically infers the correct property based on the element type.
|
|
3417
|
-
|
|
3418
|
-
### Why Infer?
|
|
3419
|
-
|
|
3420
|
-
Different HTML elements use different properties to represent their value:
|
|
3421
|
-
- Input text fields use `value`
|
|
3422
|
-
- Checkboxes and radio buttons use `checked`
|
|
3423
|
-
- Time elements use `dateTime`
|
|
3424
|
-
- Divs and spans use `textContent`
|
|
3425
|
-
- Progress and meter elements use `value` but display with `ariaValueText`
|
|
3426
|
-
|
|
3427
|
-
The Infer enhancement eliminates the need to remember these differences by providing two symbols that automatically map to the correct property:
|
|
3428
|
-
|
|
3429
|
-
- `value` symbol - Sets the element's data value
|
|
3430
|
-
- `display` symbol - Sets the element's display/presentation value
|
|
3431
|
-
|
|
3432
|
-
### Basic Usage
|
|
3433
|
-
|
|
3434
|
-
```TypeScript
|
|
3435
|
-
import { value, display, registryItem } from 'assign-gingerly/Infer.js';
|
|
3436
|
-
import 'assign-gingerly/object-extension.js';
|
|
3437
|
-
|
|
3438
|
-
// Register the Infer enhancement
|
|
3439
|
-
customElements.enhancementRegistry.push(registryItem);
|
|
3440
|
-
|
|
3441
|
-
// Use the value symbol - automatically sets the right property
|
|
3442
|
-
const input = document.createElement('input');
|
|
3443
|
-
input.type = 'text';
|
|
3444
|
-
input.set[value] = 'hello';
|
|
3445
|
-
console.log(input.value); // 'hello'
|
|
3446
|
-
|
|
3447
|
-
const checkbox = document.createElement('input');
|
|
3448
|
-
checkbox.type = 'checkbox';
|
|
3449
|
-
checkbox.set[value] = true;
|
|
3450
|
-
console.log(checkbox.checked); // true
|
|
3451
|
-
|
|
3452
|
-
const div = document.createElement('div');
|
|
3453
|
-
div.set[value] = 'content';
|
|
3454
|
-
console.log(div.textContent); // 'content'
|
|
3455
|
-
|
|
3456
|
-
const time = document.createElement('time');
|
|
3457
|
-
time.set[value] = '2024-01-01T00:00:00Z';
|
|
3458
|
-
console.log(time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3459
|
-
```
|
|
3460
|
-
|
|
3461
|
-
### Value Property Inference
|
|
3462
|
-
|
|
3463
|
-
The `value` symbol automatically maps to the most appropriate property for each element type:
|
|
3464
|
-
|
|
3465
|
-
| Element Type | Property Set | Example |
|
|
3466
|
-
|-------------|-------------|---------|
|
|
3467
|
-
| `<input type="text">` | `value` | Text input value |
|
|
3468
|
-
| `<input type="checkbox">` | `checked` | Checkbox state |
|
|
3469
|
-
| `<input type="radio">` | `checked` | Radio button state |
|
|
3470
|
-
| `<textarea>` | `value` | Textarea content |
|
|
3471
|
-
| `<select>` | `value` | Selected option |
|
|
3472
|
-
| `<time>` | `dateTime` | ISO datetime string |
|
|
3473
|
-
| `<data>` | `value` | Machine-readable value |
|
|
3474
|
-
| `<meter>` | `value` | Numeric value |
|
|
3475
|
-
| `<progress>` | `value` | Progress value |
|
|
3476
|
-
| `<output>` | `value` | Output value |
|
|
3477
|
-
| Elements with `itemprop` | `itemprop` value | Custom property name |
|
|
3478
|
-
| Other elements | `textContent` | Text content |
|
|
3479
|
-
|
|
3480
|
-
### Display Property Inference
|
|
3481
|
-
|
|
3482
|
-
The `display` symbol sets the human-readable display value:
|
|
3483
|
-
|
|
3484
|
-
```TypeScript
|
|
3485
|
-
// Time element - display formatted time
|
|
3486
|
-
const time = document.createElement('time');
|
|
3487
|
-
time.set[value] = '2024-01-01T00:00:00Z'; // Machine-readable
|
|
3488
|
-
time.set[display] = 'January 1, 2024'; // Human-readable
|
|
3489
|
-
console.log(time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3490
|
-
console.log(time.textContent); // 'January 1, 2024'
|
|
3491
|
-
|
|
3492
|
-
// Meter element - display with ARIA
|
|
3493
|
-
const meter = document.createElement('meter');
|
|
3494
|
-
meter.min = 0;
|
|
3495
|
-
meter.max = 100;
|
|
3496
|
-
meter.set[value] = 75; // Numeric value
|
|
3497
|
-
meter.set[display] = '75 percent'; // Screen reader text
|
|
3498
|
-
console.log(meter.value); // 75
|
|
3499
|
-
console.log(meter.ariaValueText); // '75 percent'
|
|
3500
|
-
```
|
|
3501
|
-
|
|
3502
|
-
| Element Type | Property Set | Example |
|
|
3503
|
-
|-------------|-------------|---------|
|
|
3504
|
-
| `<input>`, `<textarea>`, `<select>` | `value` | Form control value |
|
|
3505
|
-
| `<time>` | `textContent` | Formatted time string |
|
|
3506
|
-
| `<data>` | `textContent` | Human-readable content |
|
|
3507
|
-
| `<meter>`, `<progress>` | `ariaValueText` | Screen reader text |
|
|
3508
|
-
| Other elements | `textContent` | Text content |
|
|
3509
|
-
|
|
3510
|
-
### Accessing the Enhancement Instance
|
|
3511
|
-
|
|
3512
|
-
The Infer enhancement is accessible via `element.enh.infer`:
|
|
3513
|
-
|
|
3514
|
-
```TypeScript
|
|
3515
|
-
const input = document.createElement('input');
|
|
3516
|
-
input.set[value] = 'test';
|
|
3517
|
-
|
|
3518
|
-
// Access the enhancement instance
|
|
3519
|
-
console.log(input.enh.infer.value); // 'test' (cached value)
|
|
3520
|
-
|
|
3521
|
-
// The instance maintains references to both value and display
|
|
3522
|
-
input.set[display] = 'Test Display';
|
|
3523
|
-
console.log(input.enh.infer.value); // 'test'
|
|
3524
|
-
console.log(input.enh.infer.display); // 'Test Display'
|
|
3525
|
-
```
|
|
3526
|
-
|
|
3527
|
-
### Using with assignGingerly
|
|
3528
|
-
|
|
3529
|
-
The Infer enhancement integrates seamlessly with `assignGingerly`:
|
|
3530
|
-
|
|
3531
|
-
```TypeScript
|
|
3532
|
-
import { value, display } from 'assign-gingerly/Infer.js';
|
|
3533
|
-
|
|
3534
|
-
const element = document.createElement('input');
|
|
3535
|
-
element.type = 'text';
|
|
3536
|
-
|
|
3537
|
-
// Use symbols in assignGingerly
|
|
3538
|
-
element.assignGingerly({
|
|
3539
|
-
[value]: 'hello world',
|
|
3540
|
-
style: {
|
|
3541
|
-
color: 'blue'
|
|
3542
|
-
}
|
|
3543
|
-
});
|
|
3544
|
-
|
|
3545
|
-
console.log(element.value); // 'hello world'
|
|
3546
|
-
console.log(element.style.color); // 'blue'
|
|
3547
|
-
```
|
|
3548
|
-
|
|
3549
|
-
### Itemprop Support
|
|
3550
|
-
|
|
3551
|
-
Elements with an `itemprop` attribute use that attribute's value as the property name:
|
|
3552
|
-
|
|
3553
|
-
```html
|
|
3554
|
-
<span itemprop="title"></span>
|
|
3555
|
-
```
|
|
3556
|
-
|
|
3557
|
-
```TypeScript
|
|
3558
|
-
const span = document.querySelector('[itemprop="title"]');
|
|
3559
|
-
span.set[value] = 'My Title';
|
|
3560
|
-
console.log(span.title); // 'My Title'
|
|
3561
|
-
```
|
|
3562
|
-
|
|
3563
|
-
### Implementation Details
|
|
3564
|
-
|
|
3565
|
-
The Infer enhancement is implemented as a standard enhancement class:
|
|
3566
|
-
|
|
3567
|
-
```TypeScript
|
|
3568
|
-
class Infer<TValue = any, TDisplay = any> {
|
|
3569
|
-
#weakRef: WeakRef<Element>;
|
|
3570
|
-
|
|
3571
|
-
constructor(enhancedElement?: Element) {
|
|
3572
|
-
this.#weakRef = new WeakRef(enhancedElement!);
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
get value(): TValue | undefined { /* ... */ }
|
|
3576
|
-
set value(nv: TValue) {
|
|
3577
|
-
const element = this.#weakRef.deref()!;
|
|
3578
|
-
element[inferValueProperty(element)] = nv;
|
|
3579
|
-
}
|
|
3580
|
-
|
|
3581
|
-
get display(): TDisplay | undefined { /* ... */ }
|
|
3582
|
-
set display(nv: TDisplay) {
|
|
3583
|
-
const element = this.#weakRef.deref()!;
|
|
3584
|
-
element[inferDisplayProperty(element)] = nv;
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
```
|
|
3588
|
-
|
|
3589
|
-
**Registry Configuration:**
|
|
3590
|
-
|
|
3591
|
-
```TypeScript
|
|
3592
|
-
export const registryItem: EnhancementConfig = {
|
|
3593
|
-
spawn: Infer,
|
|
3594
|
-
enhKey: 'infer',
|
|
3595
|
-
symlinks: {
|
|
3596
|
-
[value]: 'value',
|
|
3597
|
-
[display]: 'display'
|
|
3598
|
-
}
|
|
3599
|
-
};
|
|
3600
|
-
```
|
|
3601
|
-
|
|
3602
|
-
The `symlinks` mapping connects the symbols to the enhancement's properties, enabling the `element.set[symbol]` syntax.
|
|
3603
|
-
|
|
3604
|
-
### Helper Functions
|
|
3605
|
-
|
|
3606
|
-
The Infer module exports helper functions for manual property and event type inference:
|
|
3607
|
-
|
|
3608
|
-
```TypeScript
|
|
3609
|
-
import { inferValueProperty, inferDisplayProperty, inferEventType } from 'assign-gingerly/Infer.js';
|
|
3610
|
-
|
|
3611
|
-
const input = document.createElement('input');
|
|
3612
|
-
input.type = 'checkbox';
|
|
3613
|
-
|
|
3614
|
-
const valueProp = inferValueProperty(input);
|
|
3615
|
-
console.log(valueProp); // 'checked'
|
|
3616
|
-
|
|
3617
|
-
const displayProp = inferDisplayProperty(input);
|
|
3618
|
-
console.log(displayProp); // 'value'
|
|
3619
|
-
|
|
3620
|
-
const eventType = inferEventType(input);
|
|
3621
|
-
console.log(eventType); // 'input'
|
|
3622
|
-
```
|
|
3623
|
-
|
|
3624
|
-
These functions can be useful when you need to determine the property or event type name without actually setting a value or attaching a listener.
|
|
3625
|
-
|
|
3626
|
-
**Event Type Inference:**
|
|
3627
|
-
|
|
3628
|
-
The `inferEventType` function returns the most appropriate event type for different element types:
|
|
3629
|
-
|
|
3630
|
-
| Element Type | Event Type | Use Case |
|
|
3631
|
-
|-------------|-----------|----------|
|
|
3632
|
-
| `<input>`, `<textarea>`, `<select>` | `input` | Form control value changes |
|
|
3633
|
-
| `<form>` | `submit` | Form submission |
|
|
3634
|
-
| `<details>` | `toggle` | Details element open/close |
|
|
3635
|
-
| `<dialog>` | `close` | Dialog dismissal |
|
|
3636
|
-
| Other elements | `click` | Default interactive event |
|
|
3637
|
-
|
|
3638
|
-
**Accessing via Enhancement Instance:**
|
|
3639
|
-
|
|
3640
|
-
The inferred event type is also available as a getter on the enhancement instance:
|
|
3641
|
-
|
|
3642
|
-
```TypeScript
|
|
3643
|
-
const input = document.createElement('input');
|
|
3644
|
-
input.set[value] = 'test';
|
|
3645
|
-
|
|
3646
|
-
console.log(input.enh.infer.eventType); // 'input'
|
|
3647
|
-
|
|
3648
|
-
const form = document.createElement('form');
|
|
3649
|
-
form.set[value] = 'test';
|
|
3650
|
-
|
|
3651
|
-
console.log(form.enh.infer.eventType); // 'submit'
|
|
3652
|
-
```
|
|
3653
|
-
|
|
3654
|
-
This is particularly useful when building enhancements that need to attach event listeners but don't know the element type in advance.
|
|
3655
|
-
|
|
3656
|
-
### Benefits
|
|
3657
|
-
|
|
3658
|
-
1. **Type-agnostic code**: Write code that works with any element type without conditionals
|
|
3659
|
-
2. **Cleaner syntax**: No need to remember which property each element type uses
|
|
3660
|
-
3. **Accessibility**: Separate value and display properties support screen readers
|
|
3661
|
-
4. **Framework-friendly**: Symbols work well with reactive frameworks and data binding
|
|
3662
|
-
5. **Extensible**: Based on the enhancement registry system, can be customized or extended
|
|
3663
|
-
|
|
3664
|
-
### Complete Example
|
|
3665
|
-
|
|
3666
|
-
```html
|
|
3667
|
-
<!DOCTYPE html>
|
|
3668
|
-
<html>
|
|
3669
|
-
<head>
|
|
3670
|
-
<script type="module">
|
|
3671
|
-
import { value, display, registryItem } from './Infer.js';
|
|
3672
|
-
import './object-extension.js';
|
|
3673
|
-
|
|
3674
|
-
// Register the enhancement
|
|
3675
|
-
customElements.enhancementRegistry.push(registryItem);
|
|
3676
|
-
|
|
3677
|
-
// Create various elements
|
|
3678
|
-
const input = document.createElement('input');
|
|
3679
|
-
input.type = 'text';
|
|
3680
|
-
|
|
3681
|
-
const checkbox = document.createElement('input');
|
|
3682
|
-
checkbox.type = 'checkbox';
|
|
3683
|
-
|
|
3684
|
-
const time = document.createElement('time');
|
|
3685
|
-
|
|
3686
|
-
const meter = document.createElement('meter');
|
|
3687
|
-
meter.min = 0;
|
|
3688
|
-
meter.max = 100;
|
|
3689
|
-
|
|
3690
|
-
// Set values using the same symbol - each element handles it correctly
|
|
3691
|
-
input.set[value] = 'Hello World';
|
|
3692
|
-
checkbox.set[value] = true;
|
|
3693
|
-
time.set[value] = '2024-01-01T00:00:00Z';
|
|
3694
|
-
time.set[display] = 'January 1, 2024';
|
|
3695
|
-
meter.set[value] = 75;
|
|
3696
|
-
meter.set[display] = '75 percent';
|
|
3697
|
-
|
|
3698
|
-
// Add to document
|
|
3699
|
-
document.body.append(input, checkbox, time, meter);
|
|
3700
|
-
|
|
3701
|
-
console.log('Input value:', input.value); // 'Hello World'
|
|
3702
|
-
console.log('Checkbox checked:', checkbox.checked); // true
|
|
3703
|
-
console.log('Time dateTime:', time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3704
|
-
console.log('Time display:', time.textContent); // 'January 1, 2024'
|
|
3705
|
-
console.log('Meter value:', meter.value); // 75
|
|
3706
|
-
console.log('Meter display:', meter.ariaValueText); // '75 percent'
|
|
3707
|
-
</script>
|
|
3708
|
-
</head>
|
|
3709
|
-
<body>
|
|
3710
|
-
<h1>Infer Enhancement Demo</h1>
|
|
3711
|
-
</body>
|
|
3712
|
-
</html>
|
|
3713
|
-
```
|
|
3714
|
-
|
|
3715
|
-
### Browser Support
|
|
3716
|
-
|
|
3717
|
-
The Infer enhancement requires:
|
|
3718
|
-
- Chrome 146+ (for scoped custom element registries)
|
|
3719
|
-
- Modern browsers with Symbol support
|
|
3720
|
-
- WeakRef support (all modern browsers)
|
|
3721
|
-
|
|
3722
|
-
For browsers without scoped registry support, the enhancement falls back to the global `customElements.enhancementRegistry`.
|