assign-gingerly 0.0.31 → 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 CHANGED
@@ -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 shadowRoot = {
418
- querySelector(selector) {
419
- return this.elements[selector];
420
- },
421
- elements: {
422
- 'my-element': document.createElement('div')
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(shadowRoot, {
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: shadowRoot.querySelector('my-element').classList.add('highlighted')
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 IEnhancementRegistryItem<T = any, TObjToExtend = any> {
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(IEnhancementRegistryItem | IEnhancementRegistryItem[]){
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: IEnhancementRegistryItem<T>;
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 IEnhancementRegistryItem<T, TObj = Element> {
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
 
@@ -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: IEnhancementRegistryItem<T> }`
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
package/assignFrom.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolve RHS path strings against a source object, then assign the
3
+ * resolved values into a target using assignGingerly.
4
+ *
5
+ * Combines resolveValues + assignGingerly into a single call.
6
+ * Inherits all assignGingerly options (withMethods, aka, signal, etc.).
7
+ *
8
+ * @param target - Object to merge resolved values into
9
+ * @param pattern - Object whose RHS values may contain `?.` path strings
10
+ * @param options - Options including `from` (source object) and any assignGingerly options
11
+ * @returns The target object after merging
12
+ *
13
+ * @example
14
+ * const source = { theme: { color: 'red' }, label: 'Hello' };
15
+ * const target = { color: 'blue', text: '' };
16
+ * assignFrom(target, {
17
+ * color: '?.theme?.color',
18
+ * text: '?.label'
19
+ * }, { from: source });
20
+ * // target is now { color: 'red', text: 'Hello' }
21
+ */
22
+ import { resolveValues } from './resolveValues.js';
23
+ import assignGingerly from './assignGingerly.js';
24
+ export function assignFrom(target, pattern, options) {
25
+ const resolved = resolveValues(pattern, options.from);
26
+ return assignGingerly(target, resolved, options);
27
+ }
package/assignFrom.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Resolve RHS path strings against a source object, then assign the
3
+ * resolved values into a target using assignGingerly.
4
+ *
5
+ * Combines resolveValues + assignGingerly into a single call.
6
+ * Inherits all assignGingerly options (withMethods, aka, signal, etc.).
7
+ *
8
+ * @param target - Object to merge resolved values into
9
+ * @param pattern - Object whose RHS values may contain `?.` path strings
10
+ * @param options - Options including `from` (source object) and any assignGingerly options
11
+ * @returns The target object after merging
12
+ *
13
+ * @example
14
+ * const source = { theme: { color: 'red' }, label: 'Hello' };
15
+ * const target = { color: 'blue', text: '' };
16
+ * assignFrom(target, {
17
+ * color: '?.theme?.color',
18
+ * text: '?.label'
19
+ * }, { from: source });
20
+ * // target is now { color: 'red', text: 'Hello' }
21
+ */
22
+ import { resolveValues } from './resolveValues.js';
23
+ import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
24
+
25
+ export interface AssignFromOptions extends IAssignGingerlyOptions {
26
+ /** Source object to resolve RHS path strings against */
27
+ from: any;
28
+ }
29
+
30
+ export function assignFrom(
31
+ target: any,
32
+ pattern: Record<string, any>,
33
+ options: AssignFromOptions
34
+ ): any {
35
+ const resolved = resolveValues(pattern, options.from);
36
+ return assignGingerly(target, resolved, options);
37
+ }