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 CHANGED
@@ -1,4 +1,4 @@
1
- # assign-gingerly and assign-tentatively
1
+ # assign-gingerly and assign-tentatively
2
2
 
3
3
  [![Playwright Tests](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml/badge.svg?branch=baseline)](https://github.com/bahrus/assign-gingerly/actions/workflows/CI.yml)
4
4
  [![NPM version](https://badge.fury.io/js/assign-gingerly.png)](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 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
 
@@ -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
- // Don't dispose - enhancement should persist
1817
+ // ? Don't dispose - enhancement should persist
1464
1818
 
1465
1819
  // Case 2: Moved to another location
1466
1820
  otherContainer.append(element);
1467
- // Don't dispose - enhancement should persist
1821
+ // ? Don't dispose - enhancement should persist
1468
1822
 
1469
1823
  // Case 3: Cached for reuse
1470
1824
  elementCache.set('myElement', element);
1471
- // Don't dispose - enhancement should persist
1825
+ // ? Don't dispose - enhancement should persist
1472
1826
 
1473
1827
  // Case 4: Truly done, ready for GC
1474
1828
  element = null;
1475
- // Should dispose, but no way to detect this automatically
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
- - Storage mechanism prevents memory leaks via WeakMap
1570
- - ⚠️ Enhancement internals need manual cleanup via dispose()
1571
- - No automatic way to detect when disposal should happen
1572
- - 👍 Choose disposal strategy based on your application's lifecycle
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: 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
@@ -1976,7 +2330,7 @@ console.log(instance.value); // 'test123' (parsed from attribute)
1976
2330
 
1977
2331
  </details>
1978
2332
 
1979
- > ![NOTE]
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: '🎨-theme' }); // Has non-ASCII (and dash)
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
- - **JSON serializable** - Configs can be stored/transmitted as JSON
2380
- - **Reusable** - Define once, use everywhere
2381
- - **Maintainable** - Update parser logic in one place
2382
- - **Testable** - Test parsers independently
2383
- - **Discoverable** - `globalParserRegistry.getNames()` lists all available parsers
2384
- - **Backward compatible** - Inline functions still work
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`.