assign-gingerly 0.0.84 → 0.0.86
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/DX/emojis.js +5 -3
- package/DX/emojis.ts +5 -3
- package/DX/paths.js +2 -2
- package/DX/paths.ts +23 -23
- package/README.md +46 -1218
- package/ScopedParserRegistry.js +2 -2
- package/ScopedParserRegistry.ts +6 -6
- package/SplitParser.js +48 -0
- package/SplitParser.ts +89 -0
- package/assignFrom.js +78 -2
- package/assignFrom.ts +80 -2
- package/inferencer/types/NewCustomElementFeature.md +3 -1
- package/inferencer/types/NewHTMLFirstCustomElement.md +68 -1
- package/inferencer/types/assign-gingerly/types.d.ts +43 -5
- package/package.json +13 -4
- package/parseWithAttrs.js +109 -25
- package/parseWithAttrs.ts +135 -31
- package/parserRegistry.js +5 -4
- package/parserRegistry.ts +10 -12
- package/processHandlerCommands.js +5 -55
- package/processHandlerCommands.ts +6 -51
- package/{handlers → syncOps}/join.js +61 -73
- package/syncOps/join.ts +62 -0
- package/syncOps/registry.js +15 -0
- package/syncOps/registry.ts +19 -0
- package/types/assign-gingerly/types.d.ts +43 -5
- package/utils/resolveLhsPath.js +68 -0
- package/utils/resolveLhsPath.ts +81 -0
- package/handlers/join.ts +0 -79
package/README.md
CHANGED
|
@@ -851,7 +851,8 @@ While we are in the business of passing values of object A into object B, we mig
|
|
|
851
851
|
| ` -=` | Delete | Remove properties from an object | `'?.data -=': 'key'` |
|
|
852
852
|
| ` Y=` | Merge | Recursively `assignGingerly` into a sub-object | `'style Y=': { width: '100px' }` |
|
|
853
853
|
| ` ?=` | Ternary | Conditional assignment (assignFrom only) — [details](docs/ternary-assignment.md) | `'?.text ?=': ['?.cond', 'yes', 'no']` |
|
|
854
|
-
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.
|
|
854
|
+
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.lazyLoad', ... }` |
|
|
855
|
+
| ` =&` | Sync op | Compute a value synchronously and assign it back (assignFrom only) — no dynamic import, no await, ever | `'?.text =&': { join: ['?.first', ' ', '?.last'] }` |
|
|
855
856
|
|
|
856
857
|
All operators use a space before the suffix to distinguish them from property names. They compose with `?.` nested paths and `withMethods`.
|
|
857
858
|
|
|
@@ -1641,7 +1642,7 @@ interface EnhancementConfig<T, TObj = Element> {
|
|
|
1641
1642
|
}
|
|
1642
1643
|
```
|
|
1643
1644
|
|
|
1644
|
-
The `withAttrs` property enables automatic attribute parsing when the enhancement is spawned. See
|
|
1645
|
+
The `withAttrs` property enables automatic attribute parsing when the enhancement is spawned. See [docs/withAttrs.md](docs/withAttrs.md) for the full attribute-patterns reference.
|
|
1645
1646
|
|
|
1646
1647
|
It also tips off extending polyfills / libraries, in particular mount-observer, to be on the 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.
|
|
1647
1648
|
|
|
@@ -2443,1155 +2444,22 @@ assignGingerly(element, { [enhSymbol]: 'test' }, { registry });
|
|
|
2443
2444
|
|
|
2444
2445
|
</details>
|
|
2445
2446
|
|
|
2446
|
-
## Parsing Attributes with `
|
|
2447
|
+
## Parsing HTML Attributes with `withAttrs`
|
|
2447
2448
|
|
|
2448
|
-
|
|
2449
|
+
Enhancements and custom element features can declare attribute patterns that are automatically parsed from the host element into `initVals` when the enhancement/feature is spawned. The `withAttrs` configuration supports typed parsing, template variables, default values, custom parsers, and the `sourceOfTruth` marker consumed by higher-level features such as [truth-sourcer](https://github.com/bahrus/truth-sourcer).
|
|
2449
2450
|
|
|
2450
|
-
|
|
2451
|
+
See [docs/withAttrs.md](docs/withAttrs.md) for the full reference, including:
|
|
2451
2452
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
```TypeScript
|
|
2459
|
-
import 'assign-gingerly/object-extension.js';
|
|
2460
|
-
|
|
2461
|
-
class MyEnhancement {
|
|
2462
|
-
elementRef;
|
|
2463
|
-
ctx;
|
|
2464
|
-
count = 0;
|
|
2465
|
-
theme = 'light';
|
|
2466
|
-
|
|
2467
|
-
constructor(oElement, ctx, initVals) {
|
|
2468
|
-
this.element = new WeakRef(oElement);
|
|
2469
|
-
this.ctx = ctx;
|
|
2470
|
-
// initVals automatically contains parsed attributes!
|
|
2471
|
-
if (initVals) {
|
|
2472
|
-
Object.assign(this, initVals);
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
const element = document.querySelector('my-element');
|
|
2478
|
-
const enhancementConfig = {
|
|
2479
|
-
spawn: MyEnhancement,
|
|
2480
|
-
enhKey: 'myEnh',
|
|
2481
|
-
withAttrs: {
|
|
2482
|
-
base: 'my-enhancement',
|
|
2483
|
-
count: '${base}-count',
|
|
2484
|
-
_count: { instanceOf: 'Number' },
|
|
2485
|
-
theme: '${base}-theme'
|
|
2486
|
-
|
|
2487
|
-
}
|
|
2488
|
-
};
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
// Spawn the enhancement - attributes are automatically parsed!
|
|
2492
|
-
const instance = element.enh.get(enhancementConfig);
|
|
2493
|
-
console.log(instance.count); // 42 (parsed from attribute)
|
|
2494
|
-
console.log(instance.theme); // 'dark' (parsed from attribute)
|
|
2495
|
-
```
|
|
2496
|
-
|
|
2497
|
-
<details>
|
|
2498
|
-
<summary>Example without enhKey</summary>
|
|
2499
|
-
|
|
2500
|
-
```TypeScript
|
|
2501
|
-
// withAttrs works even without enhKey
|
|
2502
|
-
class SimpleEnhancement {
|
|
2503
|
-
element;
|
|
2504
|
-
ctx;
|
|
2505
|
-
value = null;
|
|
2506
|
-
|
|
2507
|
-
constructor(oElement, ctx, initVals) {
|
|
2508
|
-
this.element = oElement;
|
|
2509
|
-
this.ctx = ctx;
|
|
2510
|
-
if (initVals) {
|
|
2511
|
-
Object.assign(this, initVals);
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
const element = document.createElement('div');
|
|
2517
|
-
element.setAttribute('data-value', 'test123');
|
|
2518
|
-
|
|
2519
|
-
const config = {
|
|
2520
|
-
spawn: SimpleEnhancement,
|
|
2521
|
-
// No enhKey - attributes still parsed!
|
|
2522
|
-
withAttrs: {
|
|
2523
|
-
base: 'data-',
|
|
2524
|
-
value: '${base}value'
|
|
2525
|
-
}
|
|
2526
|
-
};
|
|
2527
|
-
|
|
2528
|
-
const instance = element.enh.get(config);
|
|
2529
|
-
console.log(instance.value); // 'test123' (parsed from attribute)
|
|
2530
|
-
```
|
|
2531
|
-
|
|
2532
|
-
</details>
|
|
2533
|
-
|
|
2534
|
-
<details>
|
|
2535
|
-
<summary>How it works</summary>
|
|
2536
|
-
|
|
2537
|
-
1. When an enhancement is spawned via `enh.get()`, `enh.set`, or `assignGingerly()`
|
|
2538
|
-
2. If the registry item has a `withAttrs` property defined
|
|
2539
|
-
3. `parseWithAttrs(element, registryItem.withAttrs)` is automatically called
|
|
2540
|
-
4. The parsed attributes are passed to the enhancement constructor as `initVals`
|
|
2541
|
-
5. If the registry item also has an `enhKey`, the parsed attributes are merged with any existing values from `element.enh[enhKey]` (existing values take precedence)
|
|
2542
|
-
|
|
2543
|
-
</details>
|
|
2544
|
-
|
|
2545
|
-
> [!NOTE]
|
|
2546
|
-
> `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.
|
|
2547
|
-
|
|
2548
|
-
### The `enh-` Prefix for Attribute Isolation
|
|
2549
|
-
|
|
2550
|
-
The `parseWithAttrs` function supports an `enh-` prefix for attributes to provide better isolation and avoid conflicts, especially for custom elements and SVG elements.
|
|
2551
|
-
|
|
2552
|
-
**Behavior by Element Type:**
|
|
2553
|
-
|
|
2554
|
-
- **Built-in HTML elements** (div, span, etc.): The `enh-` prefix acts as an **alias**. The function tries `enh-` prefixed attributes first, then falls back to unprefixed attributes.
|
|
2555
|
-
```html
|
|
2556
|
-
<!-- Both work for built-in elements -->
|
|
2557
|
-
<div data-count="42"></div>
|
|
2558
|
-
<div enh-data-count="42"></div>
|
|
2559
|
-
|
|
2560
|
-
<!-- enh- prefix takes precedence -->
|
|
2561
|
-
<div data-count="10" enh-data-count="42"></div> <!-- Uses 42 -->
|
|
2562
|
-
```
|
|
2563
|
-
|
|
2564
|
-
- **Custom elements and SVG elements**: The `enh-` prefix is **strictly enforced** by default. Only `enh-` prefixed attributes are read.
|
|
2565
|
-
```html
|
|
2566
|
-
<!-- Only enh- prefixed attributes work -->
|
|
2567
|
-
<my-element data-count="42"></my-element> <!-- Ignored -->
|
|
2568
|
-
<my-element enh-data-count="42"></my-element> <!-- Works -->
|
|
2569
|
-
|
|
2570
|
-
<svg enh-data-theme="dark"></svg> <!-- Works -->
|
|
2571
|
-
<svg data-theme="dark"></svg> <!-- Ignored -->
|
|
2572
|
-
```
|
|
2573
|
-
|
|
2574
|
-
**Overriding with `allowUnprefixed`:**
|
|
2575
|
-
|
|
2576
|
-
For custom elements and SVG, you can opt-in to reading unprefixed attributes by specifying a pattern (string or RegExp) that the element's tag name must match:
|
|
2577
|
-
|
|
2578
|
-
```TypeScript
|
|
2579
|
-
// Allow unprefixed for elements matching pattern
|
|
2580
|
-
registry.push({
|
|
2581
|
-
spawn: MyEnhancement,
|
|
2582
|
-
enhKey: 'myEnh',
|
|
2583
|
-
allowUnprefixed: '^my-', // Only for elements starting with "my-"
|
|
2584
|
-
withAttrs: {
|
|
2585
|
-
base: 'data-',
|
|
2586
|
-
count: '${base}count',
|
|
2587
|
-
_count: { instanceOf: 'Number' }
|
|
2588
|
-
}
|
|
2589
|
-
});
|
|
2590
|
-
|
|
2591
|
-
// Or use RegExp for more complex patterns
|
|
2592
|
-
registry.push({
|
|
2593
|
-
spawn: MyEnhancement,
|
|
2594
|
-
enhKey: 'myEnh',
|
|
2595
|
-
allowUnprefixed: /^(my-|app-)/, // For "my-*" or "app-*" elements
|
|
2596
|
-
withAttrs: {
|
|
2597
|
-
base: 'data-',
|
|
2598
|
-
count: '${base}count',
|
|
2599
|
-
_count: { instanceOf: 'Number' }
|
|
2600
|
-
}
|
|
2601
|
-
});
|
|
2602
|
-
```
|
|
2603
|
-
|
|
2604
|
-
<details>
|
|
2605
|
-
<summary>Why use `enh-` prefix?</summary>
|
|
2606
|
-
|
|
2607
|
-
1. **Avoid conflicts**: Custom elements may use unprefixed attributes for their own purposes
|
|
2608
|
-
2. **Clear intent**: Makes it obvious which attributes are for enhancements
|
|
2609
|
-
3. **Future-proof**: Protects against future attribute additions to custom elements
|
|
2610
|
-
4. **Consistency**: Provides a standard convention across all enhanced elements
|
|
2611
|
-
5. **Selective override**: Pattern-based `allowUnprefixed` lets you opt-in specific element families while maintaining strict isolation for others
|
|
2612
|
-
|
|
2613
|
-
</details>
|
|
2614
|
-
|
|
2615
|
-
<details>
|
|
2616
|
-
<summary>Manual Usage</summary>
|
|
2617
|
-
|
|
2618
|
-
While automatic parsing is the recommended approach, you can also call `parseWithAttrs()` manually when needed.
|
|
2619
|
-
|
|
2620
|
-
When calling `parseWithAttrs()` manually, pass the pattern as the third (optional) parameter:
|
|
2621
|
-
|
|
2622
|
-
```TypeScript
|
|
2623
|
-
// Allow unprefixed only for elements matching pattern
|
|
2624
|
-
const result = parseWithAttrs(element, attrPatterns, '^my-');
|
|
2625
|
-
|
|
2626
|
-
// Or with RegExp
|
|
2627
|
-
const result = parseWithAttrs(element, attrPatterns, /^(my-|app-)/);
|
|
2628
|
-
```
|
|
2629
|
-
|
|
2630
|
-
**Pattern Matching:**
|
|
2631
|
-
- The pattern is tested against the element's **lowercase tag name**
|
|
2632
|
-
- String patterns are automatically converted to RegExp
|
|
2633
|
-
- If the tag name matches, unprefixed attributes are allowed (but `enh-` still takes precedence)
|
|
2634
|
-
- If the tag name doesn't match, only `enh-` prefixed attributes are read
|
|
2635
|
-
|
|
2636
|
-
**Example:**
|
|
2637
|
-
```html
|
|
2638
|
-
<my-widget data-count="42"></my-widget>
|
|
2639
|
-
<other-widget data-count="42"></other-widget>
|
|
2640
|
-
```
|
|
2641
|
-
|
|
2642
|
-
```TypeScript
|
|
2643
|
-
// Pattern: '^my-' (only matches "my-widget")
|
|
2644
|
-
const result1 = parseWithAttrs(
|
|
2645
|
-
document.querySelector('my-widget'),
|
|
2646
|
-
{ base: 'data-', count: '${base}count', _count: { instanceOf: 'Number' } },
|
|
2647
|
-
'^my-'
|
|
2648
|
-
);
|
|
2649
|
-
// result1.count = 42 (unprefixed allowed because tag matches)
|
|
2650
|
-
|
|
2651
|
-
const result2 = parseWithAttrs(
|
|
2652
|
-
document.querySelector('other-widget'),
|
|
2653
|
-
{ base: 'data-', count: '${base}count', _count: { instanceOf: 'Number' } },
|
|
2654
|
-
'^my-'
|
|
2655
|
-
);
|
|
2656
|
-
// result2.count = undefined (unprefixed ignored because tag doesn't match)
|
|
2657
|
-
```
|
|
2658
|
-
|
|
2659
|
-
### Basic Usage
|
|
2660
|
-
|
|
2661
|
-
```TypeScript
|
|
2662
|
-
import { parseWithAttrs } from 'assign-gingerly/parseWithAttrs';
|
|
2663
|
-
|
|
2664
|
-
const element = document.querySelector('#myElement');
|
|
2665
|
-
const config = parseWithAttrs(element, {
|
|
2666
|
-
base: 'data-',
|
|
2667
|
-
count: '${base}count',
|
|
2668
|
-
_count: {
|
|
2669
|
-
instanceOf: 'Number',
|
|
2670
|
-
mapsTo: 'itemCount'
|
|
2671
|
-
}
|
|
2672
|
-
});
|
|
2673
|
-
```
|
|
2674
|
-
|
|
2675
|
-
### Error Handling
|
|
2676
|
-
|
|
2677
|
-
The function throws descriptive errors for common issues:
|
|
2678
|
-
|
|
2679
|
-
```TypeScript
|
|
2680
|
-
// Circular reference
|
|
2681
|
-
parseWithAttrs(element, {
|
|
2682
|
-
a: '${b}',
|
|
2683
|
-
b: '${a}' // Error: Circular reference detected
|
|
2684
|
-
});
|
|
2685
|
-
|
|
2686
|
-
// Undefined variable
|
|
2687
|
-
parseWithAttrs(element, {
|
|
2688
|
-
name: '${missing}' // Error: Undefined template variable: missing
|
|
2689
|
-
});
|
|
2690
|
-
|
|
2691
|
-
// Invalid JSON
|
|
2692
|
-
// HTML: <div data-obj='{invalid}'></div>
|
|
2693
|
-
parseWithAttrs(element, {
|
|
2694
|
-
base: 'data-',
|
|
2695
|
-
obj: '${base}obj',
|
|
2696
|
-
_obj: { instanceOf: 'Object' }
|
|
2697
|
-
// Error: Failed to parse JSON: "{invalid}"
|
|
2698
|
-
});
|
|
2699
|
-
|
|
2700
|
-
// Invalid number
|
|
2701
|
-
// HTML: <div data-count="abc"></div>
|
|
2702
|
-
parseWithAttrs(element, {
|
|
2703
|
-
base: 'data-',
|
|
2704
|
-
count: '${base}count',
|
|
2705
|
-
_count: { instanceOf: 'Number' }
|
|
2706
|
-
// Error: Failed to parse number: "abc"
|
|
2707
|
-
});
|
|
2708
|
-
```
|
|
2709
|
-
|
|
2710
|
-
</details>
|
|
2711
|
-
|
|
2712
|
-
**Base Attribute Validation:**
|
|
2713
|
-
|
|
2714
|
-
The `base` attribute must contain either a dash (`-`) or a non-ASCII character to prevent conflicts with native attributes:
|
|
2715
|
-
|
|
2716
|
-
```TypeScript
|
|
2717
|
-
// Valid base attributes
|
|
2718
|
-
const enhConfig1 = { base: 'data-config' }; // Has dash
|
|
2719
|
-
const enhConfig2 = { base: '??-theme' }); // Has non-ASCII (and dash)
|
|
2720
|
-
|
|
2721
|
-
// Invalid - throws error
|
|
2722
|
-
const enhConig3 = { base: 'config' }; // No dash or non-ASCII
|
|
2723
|
-
```
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
<details>
|
|
2727
|
-
<summary>AttrPatterns Configuration</summary>
|
|
2728
|
-
|
|
2729
|
-
The `parseWithAttrs` function accepts an `AttrPatterns` object that defines:
|
|
2730
|
-
|
|
2731
|
-
1. **Attribute name templates**: String values with `${variable}` placeholders
|
|
2732
|
-
2. **Configuration objects**: Properties prefixed with `_` that specify parsing behavior
|
|
2733
|
-
|
|
2734
|
-
```TypeScript
|
|
2735
|
-
interface AttrPatterns<T> {
|
|
2736
|
-
base?: string; // Base attribute name prefix
|
|
2737
|
-
_base?: AttrConfig<T>; // Configuration for base attribute
|
|
2738
|
-
[key: string]: string | AttrConfig<T>; // Other attributes and configs
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
interface AttrConfig<T> {
|
|
2742
|
-
mapsTo?: keyof T | '.'; // Target property name (or '.' to spread)
|
|
2743
|
-
instanceOf?: string | Function; // Type for default parser
|
|
2744
|
-
parser?:
|
|
2745
|
-
| ((v: string | null) => any) // Inline parser function
|
|
2746
|
-
| string // Named parser from globalParserRegistry
|
|
2747
|
-
| [string, string]; // [CustomElementName, StaticMethodName]
|
|
2748
|
-
}
|
|
2749
|
-
```
|
|
2750
|
-
|
|
2751
|
-
### Template Variables
|
|
2752
|
-
|
|
2753
|
-
Attribute names support template variables using `${varName}` syntax:
|
|
2754
|
-
|
|
2755
|
-
```TypeScript
|
|
2756
|
-
// HTML: <div data-user-name="Alice" data-user-age="30"></div>
|
|
2757
|
-
|
|
2758
|
-
const result = parseWithAttrs(element, {
|
|
2759
|
-
base: 'data-',
|
|
2760
|
-
user: '${base}user',
|
|
2761
|
-
name: '${user}-name',
|
|
2762
|
-
age: '${user}-age'
|
|
2763
|
-
});
|
|
2764
|
-
// Result: { name: 'Alice', age: '30' }
|
|
2765
|
-
```
|
|
2766
|
-
|
|
2767
|
-
**Deep Nesting:**
|
|
2768
|
-
|
|
2769
|
-
Template variables can reference other template variables to any depth, creating hierarchical attribute naming patterns:
|
|
2770
|
-
|
|
2771
|
-
```TypeScript
|
|
2772
|
-
// HTML: <div data-app-user-profile-name="Alice" data-app-user-profile-email="alice@example.com"></div>
|
|
2773
|
-
|
|
2774
|
-
const result = parseWithAttrs(element, {
|
|
2775
|
-
base: 'data-',
|
|
2776
|
-
app: '${base}app',
|
|
2777
|
-
user: '${app}-user',
|
|
2778
|
-
profile: '${user}-profile',
|
|
2779
|
-
name: '${profile}-name',
|
|
2780
|
-
email: '${profile}-email'
|
|
2781
|
-
});
|
|
2782
|
-
// Result: { name: 'Alice', email: 'alice@example.com' }
|
|
2783
|
-
|
|
2784
|
-
// The resolution chain: base ? app ? user ? profile ? name/email
|
|
2785
|
-
// Resolves to: data-app-user-profile-name and data-app-user-profile-email
|
|
2786
|
-
```
|
|
2787
|
-
|
|
2788
|
-
**Benefits of hierarchical variables:**
|
|
2789
|
-
- Build complex attribute names from simple parts
|
|
2790
|
-
- Maintain consistency across related attributes
|
|
2791
|
-
- Easy to refactor by changing a single variable
|
|
2792
|
-
- Self-documenting attribute structure
|
|
2793
|
-
|
|
2794
|
-
Template variables are resolved recursively and cached for performance. Circular references are detected and throw an error.
|
|
2795
|
-
|
|
2796
|
-
### Type Parsing with instanceOf
|
|
2797
|
-
|
|
2798
|
-
The `instanceOf` property determines how attribute values are parsed:
|
|
2799
|
-
|
|
2800
|
-
```TypeScript
|
|
2801
|
-
// HTML: <div data-count="42" data-active data-tags='["a","b"]'></div>
|
|
2802
|
-
|
|
2803
|
-
const result = parseWithAttrs(element, {
|
|
2804
|
-
base: 'data-',
|
|
2805
|
-
count: '${base}count',
|
|
2806
|
-
_count: { instanceOf: 'Number' },
|
|
2807
|
-
|
|
2808
|
-
active: '${base}active',
|
|
2809
|
-
_active: { instanceOf: 'Boolean' }, // Presence check
|
|
2810
|
-
|
|
2811
|
-
tags: '${base}-tags',
|
|
2812
|
-
_tags: { instanceOf: 'Array' }
|
|
2813
|
-
});
|
|
2814
|
-
// Result: { count: 42, active: true, tags: ['a', 'b'] }
|
|
2815
|
-
```
|
|
2816
|
-
|
|
2817
|
-
**Built-in type parsers:**
|
|
2818
|
-
- `String`: Identity (default)
|
|
2819
|
-
- `Number`: Parses numeric values, throws on invalid numbers
|
|
2820
|
-
- `Boolean`: Presence check (attribute exists = true)
|
|
2821
|
-
- `Object`: Parses JSON objects
|
|
2822
|
-
- `Array`: Parses JSON arrays
|
|
2823
|
-
|
|
2824
|
-
### Custom Parsers
|
|
2825
|
-
|
|
2826
|
-
Provide a custom `parser` function for specialized parsing:
|
|
2827
|
-
|
|
2828
|
-
```TypeScript
|
|
2829
|
-
// HTML: <div data-timestamp="2024-01-15T10:30:00Z"></div>
|
|
2830
|
-
|
|
2831
|
-
const result = parseWithAttrs(element, {
|
|
2832
|
-
base: 'data-',
|
|
2833
|
-
timestamp: '${base}timestamp',
|
|
2834
|
-
_timestamp: {
|
|
2835
|
-
mapsTo: 'createdAt',
|
|
2836
|
-
parser: (v) => v ? new Date(v).getTime() : null
|
|
2837
|
-
}
|
|
2838
|
-
});
|
|
2839
|
-
// Result: { createdAt: 1705315800000 }
|
|
2840
|
-
```
|
|
2841
|
-
|
|
2842
|
-
### Named Parsers for Reusability and JSON Serialization
|
|
2843
|
-
|
|
2844
|
-
Instead of inline functions, you can reference parsers by name, making configs JSON serializable and parsers reusable:
|
|
2845
|
-
|
|
2846
|
-
```TypeScript
|
|
2847
|
-
import { globalParserRegistry, parseWithAttrs } from 'assign-gingerly';
|
|
2848
|
-
|
|
2849
|
-
// Register parsers once (typically in app initialization)
|
|
2850
|
-
globalParserRegistry.register('timestamp', (v) =>
|
|
2851
|
-
v ? new Date(v).getTime() : null
|
|
2852
|
-
);
|
|
2853
|
-
|
|
2854
|
-
globalParserRegistry.register('csv', (v) =>
|
|
2855
|
-
v ? v.split(',').map(s => s.trim()) : []
|
|
2856
|
-
);
|
|
2857
|
-
|
|
2858
|
-
// Use by name - config is now JSON serializable!
|
|
2859
|
-
const config = {
|
|
2860
|
-
base: 'data-',
|
|
2861
|
-
created: '${base}created',
|
|
2862
|
-
_created: {
|
|
2863
|
-
parser: 'timestamp' // String reference instead of function
|
|
2864
|
-
},
|
|
2865
|
-
tags: '${base}tags',
|
|
2866
|
-
_tags: {
|
|
2867
|
-
parser: 'csv'
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
|
|
2871
|
-
// Can serialize to JSON
|
|
2872
|
-
const json = JSON.stringify(config);
|
|
2873
|
-
|
|
2874
|
-
// Use the config
|
|
2875
|
-
const result = parseWithAttrs(element, config);
|
|
2876
|
-
```
|
|
2877
|
-
|
|
2878
|
-
**Built-in Named Parsers:**
|
|
2879
|
-
|
|
2880
|
-
[TODO]: Check if this is all needed
|
|
2881
|
-
|
|
2882
|
-
The following parsers are pre-registered in `globalParserRegistry`:
|
|
2883
|
-
|
|
2884
|
-
- `'timestamp'` - Parses ISO date string to Unix timestamp (milliseconds)
|
|
2885
|
-
- `'date'` - Parses string to Date object
|
|
2886
|
-
- `'csv'` - Splits comma-separated values into trimmed array
|
|
2887
|
-
- `'int'` - Parses integer with `parseInt(v, 10)`
|
|
2888
|
-
- `'float'` - Parses float with `parseFloat(v)`
|
|
2889
|
-
- `'boolean'` - Presence check (same as `instanceOf: 'Boolean'`)
|
|
2890
|
-
- `'json'` - Parses JSON (same as `instanceOf: 'Object'` or `'Array'`)
|
|
2891
|
-
|
|
2892
|
-
**Custom Element Static Method Parsers:**
|
|
2893
|
-
|
|
2894
|
-
You can reference static methods on custom elements using tuple syntax `[elementName, methodName]`:
|
|
2895
|
-
|
|
2896
|
-
```TypeScript
|
|
2897
|
-
class MyWidget extends HTMLElement {
|
|
2898
|
-
static parseSpecialFormat(v) {
|
|
2899
|
-
return v ? v.toUpperCase() : null;
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
static parseWithPrefix(v) {
|
|
2903
|
-
return v ? `PREFIX:${v}` : null;
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
customElements.define('my-widget', MyWidget);
|
|
2907
|
-
|
|
2908
|
-
// Reference custom element parsers using tuple syntax
|
|
2909
|
-
const config = {
|
|
2910
|
-
base: 'data-',
|
|
2911
|
-
value: '${base}value',
|
|
2912
|
-
_value: {
|
|
2913
|
-
parser: ['my-widget', 'parseSpecialFormat'] // [element-name, methodName]
|
|
2914
|
-
},
|
|
2915
|
-
title: '${base}title',
|
|
2916
|
-
_title: {
|
|
2917
|
-
parser: ['my-widget', 'parseWithPrefix']
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
|
|
2921
|
-
const result = parseWithAttrs(element, config);
|
|
2922
|
-
```
|
|
2923
|
-
|
|
2924
|
-
**Parser Resolution:**
|
|
2925
|
-
|
|
2926
|
-
When a parser is specified, it can be:
|
|
2927
|
-
|
|
2928
|
-
1. **Inline function** - `parser: (v) => v.toUpperCase()` - Used directly
|
|
2929
|
-
2. **String reference** - `parser: 'timestamp'` - Looks up in `globalParserRegistry`
|
|
2930
|
-
3. **Tuple reference** - `parser: ['my-widget', 'parseMethod']` - Looks up static method on custom element constructor
|
|
2931
|
-
|
|
2932
|
-
**Error Handling:**
|
|
2933
|
-
|
|
2934
|
-
The tuple syntax provides clear error messages:
|
|
2935
|
-
|
|
2936
|
-
```TypeScript
|
|
2937
|
-
// Element not found
|
|
2938
|
-
parser: ['non-existent', 'method']
|
|
2939
|
-
// Error: Cannot resolve parser [non-existent, method]: custom element "non-existent" not found
|
|
2940
|
-
|
|
2941
|
-
// Method not found
|
|
2942
|
-
parser: ['my-widget', 'nonExistent']
|
|
2943
|
-
// Error: Cannot resolve parser [my-widget, nonExistent]: static method "nonExistent" not found on custom element "my-widget"
|
|
2944
|
-
|
|
2945
|
-
// String not found in registry
|
|
2946
|
-
parser: 'unknown'
|
|
2947
|
-
// Error: Parser "unknown" not found in globalParserRegistry. If you want to reference a custom element static method, use tuple syntax: ["element-name", "methodName"]
|
|
2948
|
-
```
|
|
2949
|
-
|
|
2950
|
-
**Example: Organizing Parsers**
|
|
2951
|
-
|
|
2952
|
-
```TypeScript
|
|
2953
|
-
// parsers.js - Centralized parser definitions
|
|
2954
|
-
export function registerCommonParsers(registry) {
|
|
2955
|
-
registry.register('uppercase', (v) => v ? v.toUpperCase() : null);
|
|
2956
|
-
registry.register('lowercase', (v) => v ? v.toLowerCase() : null);
|
|
2957
|
-
registry.register('trim', (v) => v ? v.trim() : null);
|
|
2958
|
-
registry.register('phone', (v) => v ? v.replace(/\D/g, '') : null);
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
// app.js - Register at startup
|
|
2962
|
-
import { globalParserRegistry } from 'assign-gingerly';
|
|
2963
|
-
import { registerCommonParsers } from './parsers.js';
|
|
2964
|
-
|
|
2965
|
-
registerCommonParsers(globalParserRegistry);
|
|
2966
|
-
|
|
2967
|
-
// Now all configs can use these parsers by name
|
|
2968
|
-
```
|
|
2969
|
-
|
|
2970
|
-
**Benefits of Named Parsers:**
|
|
2971
|
-
|
|
2972
|
-
- ? **JSON serializable** - Configs can be stored/transmitted as JSON
|
|
2973
|
-
- ? **Reusable** - Define once, use everywhere
|
|
2974
|
-
- ? **Maintainable** - Update parser logic in one place
|
|
2975
|
-
- ? **Testable** - Test parsers independently
|
|
2976
|
-
- ? **Discoverable** - `globalParserRegistry.getNames()` lists all available parsers
|
|
2977
|
-
- ? **Backward compatible** - Inline functions still work
|
|
2978
|
-
|
|
2979
|
-
**Mixing Inline and Named Parsers:**
|
|
2980
|
-
|
|
2981
|
-
```TypeScript
|
|
2982
|
-
const config = {
|
|
2983
|
-
base: 'data-',
|
|
2984
|
-
created: '${base}created',
|
|
2985
|
-
_created: {
|
|
2986
|
-
parser: 'timestamp' // Named parser
|
|
2987
|
-
},
|
|
2988
|
-
special: '${base}special',
|
|
2989
|
-
_special: {
|
|
2990
|
-
parser: (v) => v ? v.split('').reverse().join('') : null // Inline
|
|
2991
|
-
}
|
|
2992
|
-
};
|
|
2993
|
-
```
|
|
2994
|
-
|
|
2995
|
-
### Property Mapping with mapsTo
|
|
2996
|
-
|
|
2997
|
-
The `mapsTo` property controls where parsed values are placed:
|
|
2998
|
-
|
|
2999
|
-
```TypeScript
|
|
3000
|
-
// HTML: <div data-count="5"></div>
|
|
3001
|
-
|
|
3002
|
-
const result = parseWithAttrs(element, {
|
|
3003
|
-
base: 'data-',
|
|
3004
|
-
count: '${base}count',
|
|
3005
|
-
_count: {
|
|
3006
|
-
instanceOf: 'Number',
|
|
3007
|
-
mapsTo: 'itemCount' // Maps to different property name
|
|
3008
|
-
}
|
|
3009
|
-
});
|
|
3010
|
-
// Result: { itemCount: 5 }
|
|
3011
|
-
```
|
|
3012
|
-
|
|
3013
|
-
**Special value `'.'`**: Spreads the parsed object into the root:
|
|
3014
|
-
|
|
3015
|
-
```TypeScript
|
|
3016
|
-
// HTML: <div data-config='{"theme":"dark","lang":"en"}'></div>
|
|
3017
|
-
|
|
3018
|
-
const result = parseWithAttrs(element, {
|
|
3019
|
-
base: 'data-config',
|
|
3020
|
-
_base: {
|
|
3021
|
-
instanceOf: 'Object',
|
|
3022
|
-
mapsTo: '.' // Spread into root
|
|
3023
|
-
}
|
|
3024
|
-
});
|
|
3025
|
-
// Result: { theme: 'dark', lang: 'en' }
|
|
3026
|
-
```
|
|
3027
|
-
|
|
3028
|
-
### Default Values with valIfNull
|
|
3029
|
-
|
|
3030
|
-
The `valIfNull` property allows us to specify default values when attributes are missing:
|
|
3031
|
-
|
|
3032
|
-
```TypeScript
|
|
3033
|
-
// HTML: <div></div> (no attributes)
|
|
3034
|
-
|
|
3035
|
-
const result = parseWithAttrs(element, {
|
|
3036
|
-
base: 'data-',
|
|
3037
|
-
theme: '${base}theme',
|
|
3038
|
-
_theme: {
|
|
3039
|
-
instanceOf: 'String',
|
|
3040
|
-
valIfNull: 'light' // Default when attribute is missing
|
|
3041
|
-
},
|
|
3042
|
-
count: '${base}count',
|
|
3043
|
-
_count: {
|
|
3044
|
-
instanceOf: 'Number',
|
|
3045
|
-
valIfNull: 0 // Default to 0
|
|
3046
|
-
}
|
|
3047
|
-
});
|
|
3048
|
-
// Result: { theme: 'light', count: 0 }
|
|
3049
|
-
```
|
|
3050
|
-
|
|
3051
|
-
**How it works:**
|
|
3052
|
-
- **Attribute missing**: If the attribute doesn't exist and `valIfNull` is defined, the default value is used **without calling the parser**
|
|
3053
|
-
- **Attribute present**: If the attribute exists (even if empty string), the parser is called normally and `valIfNull` is ignored
|
|
3054
|
-
- **No valIfNull**: If `valIfNull` is undefined and the attribute is missing, the property is not added to the result (current behavior)
|
|
3055
|
-
|
|
3056
|
-
**Important notes:**
|
|
3057
|
-
1. **Parser is bypassed**: When `valIfNull` is used, the parser is NOT called - the default value is used as-is
|
|
3058
|
-
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
|
|
3059
|
-
3. **Any value allowed**: `valIfNull` can be any JavaScript value: string, number, boolean, object, array, null, etc.
|
|
3060
|
-
4. **Falsy values work**: Even falsy values like `0`, `false`, `''`, or `null` are valid defaults
|
|
3061
|
-
|
|
3062
|
-
**Examples with different types:**
|
|
3063
|
-
|
|
3064
|
-
```TypeScript
|
|
3065
|
-
// Object default
|
|
3066
|
-
const result1 = parseWithAttrs(element, {
|
|
3067
|
-
base: 'config-',
|
|
3068
|
-
settings: '${base}settings',
|
|
3069
|
-
_settings: {
|
|
3070
|
-
instanceOf: 'Object',
|
|
3071
|
-
valIfNull: { enabled: false, mode: 'auto' }
|
|
3072
|
-
}
|
|
3073
|
-
});
|
|
3074
|
-
// Result: { settings: { enabled: false, mode: 'auto' } }
|
|
3075
|
-
|
|
3076
|
-
// Boolean default
|
|
3077
|
-
const result2 = parseWithAttrs(element, {
|
|
3078
|
-
base: 'feature-',
|
|
3079
|
-
enabled: '${base}enabled',
|
|
3080
|
-
_enabled: {
|
|
3081
|
-
instanceOf: 'Boolean',
|
|
3082
|
-
valIfNull: false
|
|
3083
|
-
}
|
|
3084
|
-
});
|
|
3085
|
-
// Result: { enabled: false }
|
|
3086
|
-
|
|
3087
|
-
// Array default
|
|
3088
|
-
const result3 = parseWithAttrs(element, {
|
|
3089
|
-
base: 'data-',
|
|
3090
|
-
items: '${base}items',
|
|
3091
|
-
_items: {
|
|
3092
|
-
instanceOf: 'Array',
|
|
3093
|
-
valIfNull: []
|
|
3094
|
-
}
|
|
3095
|
-
});
|
|
3096
|
-
// Result: { items: [] }
|
|
3097
|
-
|
|
3098
|
-
// null as default
|
|
3099
|
-
const result4 = parseWithAttrs(element, {
|
|
3100
|
-
base: 'data-',
|
|
3101
|
-
value: '${base}value',
|
|
3102
|
-
_value: {
|
|
3103
|
-
instanceOf: 'String',
|
|
3104
|
-
valIfNull: null
|
|
3105
|
-
}
|
|
3106
|
-
});
|
|
3107
|
-
// Result: { value: null }
|
|
3108
|
-
```
|
|
3109
|
-
|
|
3110
|
-
**Comparison: Empty string vs missing attribute:**
|
|
3111
|
-
|
|
3112
|
-
```html
|
|
3113
|
-
<!-- Attribute is missing -->
|
|
3114
|
-
<div></div>
|
|
3115
|
-
|
|
3116
|
-
<!-- Attribute exists but is empty -->
|
|
3117
|
-
<div data-count=""></div>
|
|
3118
|
-
```
|
|
3119
|
-
|
|
3120
|
-
```TypeScript
|
|
3121
|
-
const config = {
|
|
3122
|
-
base: 'data-',
|
|
3123
|
-
count: '${base}count',
|
|
3124
|
-
_count: {
|
|
3125
|
-
instanceOf: 'Number',
|
|
3126
|
-
valIfNull: 99
|
|
3127
|
-
}
|
|
3128
|
-
};
|
|
3129
|
-
|
|
3130
|
-
// Missing attribute - uses valIfNull
|
|
3131
|
-
const result1 = parseWithAttrs(document.querySelector('div:nth-child(1)'), config);
|
|
3132
|
-
// Result: { count: 99 }
|
|
3133
|
-
|
|
3134
|
-
// Empty string - calls parser (returns null for empty Number)
|
|
3135
|
-
const result2 = parseWithAttrs(document.querySelector('div:nth-child(2)'), config);
|
|
3136
|
-
// Result: { count: null }
|
|
3137
|
-
```
|
|
3138
|
-
|
|
3139
|
-
### Performance Optimization with parseCache
|
|
3140
|
-
|
|
3141
|
-
The `parseCache` property enables caching of parsed attribute values to improve performance when the same attribute values appear repeatedly throughout the document:
|
|
3142
|
-
|
|
3143
|
-
```TypeScript
|
|
3144
|
-
// HTML: Multiple elements with same attribute values
|
|
3145
|
-
// <div data-config='{"theme":"dark","size":"large"}'></div>
|
|
3146
|
-
// <div data-config='{"theme":"dark","size":"large"}'></div>
|
|
3147
|
-
// <div data-config='{"theme":"dark","size":"large"}'></div>
|
|
3148
|
-
|
|
3149
|
-
const config = {
|
|
3150
|
-
base: 'data-',
|
|
3151
|
-
config: '${base}config',
|
|
3152
|
-
_config: {
|
|
3153
|
-
instanceOf: 'Object',
|
|
3154
|
-
parseCache: 'shared' // Cache and reuse parsed objects
|
|
3155
|
-
}
|
|
3156
|
-
};
|
|
3157
|
-
|
|
3158
|
-
// First parse - parses and caches
|
|
3159
|
-
const result1 = parseWithAttrs(element1, config);
|
|
3160
|
-
|
|
3161
|
-
// Subsequent parses - returns cached value (no parsing)
|
|
3162
|
-
const result2 = parseWithAttrs(element2, config);
|
|
3163
|
-
const result3 = parseWithAttrs(element3, config);
|
|
3164
|
-
```
|
|
3165
|
-
|
|
3166
|
-
**Cache Strategies:**
|
|
3167
|
-
|
|
3168
|
-
1. **`'shared'`**: Returns the same object reference from cache
|
|
3169
|
-
- **Fastest**: No cloning overhead
|
|
3170
|
-
- **Risk**: Enhancements that mutate the object will affect all instances
|
|
3171
|
-
- **Best for**: Immutable data or when you trust enhancements not to mutate
|
|
3172
|
-
|
|
3173
|
-
2. **`'cloned'`**: Returns a structural clone of the cached object
|
|
3174
|
-
- **Safer**: Each instance gets its own copy
|
|
3175
|
-
- **Slower**: Uses `structuredClone()` which has overhead
|
|
3176
|
-
- **Best for**: Mutable data or when enhancements might modify values
|
|
3177
|
-
|
|
3178
|
-
**Examples:**
|
|
3179
|
-
|
|
3180
|
-
```TypeScript
|
|
3181
|
-
// Shared cache - fast but requires discipline
|
|
3182
|
-
const sharedConfig = {
|
|
3183
|
-
base: 'data-',
|
|
3184
|
-
settings: '${base}settings',
|
|
3185
|
-
_settings: {
|
|
3186
|
-
instanceOf: 'Object',
|
|
3187
|
-
parseCache: 'shared' // All instances share same object
|
|
3188
|
-
}
|
|
3189
|
-
};
|
|
3190
|
-
|
|
3191
|
-
// Cloned cache - safer for mutable data
|
|
3192
|
-
const clonedConfig = {
|
|
3193
|
-
base: 'data-',
|
|
3194
|
-
state: '${base}state',
|
|
3195
|
-
_state: {
|
|
3196
|
-
instanceOf: 'Object',
|
|
3197
|
-
parseCache: 'cloned' // Each instance gets a copy
|
|
3198
|
-
}
|
|
3199
|
-
};
|
|
3200
|
-
|
|
3201
|
-
// Custom parser with caching
|
|
3202
|
-
let parseCount = 0;
|
|
3203
|
-
const customConfig = {
|
|
3204
|
-
base: 'data-',
|
|
3205
|
-
timestamp: '${base}timestamp',
|
|
3206
|
-
_timestamp: {
|
|
3207
|
-
parser: (v) => {
|
|
3208
|
-
parseCount++; // Track parse calls
|
|
3209
|
-
return v ? new Date(v).getTime() : null;
|
|
3210
|
-
},
|
|
3211
|
-
parseCache: 'shared' // Parser only called once per unique value
|
|
3212
|
-
}
|
|
3213
|
-
};
|
|
3214
|
-
```
|
|
3215
|
-
|
|
3216
|
-
**Important Notes:**
|
|
3217
|
-
|
|
3218
|
-
1. **Parser purity**: Parsers should be pure functions (no side effects) when using caching
|
|
3219
|
-
2. **Boolean types**: Caching is skipped for Boolean types (presence check doesn't benefit)
|
|
3220
|
-
3. **Cache scope**: Cache is module-level and persists across all `parseWithAttrs()` calls
|
|
3221
|
-
4. **Cache key**: Values are cached per `(instanceOf, parserType, attributeValue)` tuple
|
|
3222
|
-
5. **Memory**: Cache grows with unique attribute values encountered (no automatic cleanup)
|
|
3223
|
-
6. **Browser support**: `'cloned'` strategy requires `structuredClone()` (modern browsers)
|
|
3224
|
-
|
|
3225
|
-
**Performance Considerations:**
|
|
3226
|
-
|
|
3227
|
-
- **Shared cache**: Best for simple objects, arrays, or when parsing is expensive
|
|
3228
|
-
- **Cloned cache**: Overhead may negate benefits for simple values (strings, numbers)
|
|
3229
|
-
- **No cache**: Better for unique values or when parsing is trivial
|
|
3230
|
-
- **Custom parsers**: Caching is most beneficial when parser does expensive operations (Date parsing, complex transformations)
|
|
3231
|
-
|
|
3232
|
-
**Example: Shared cache mutation risk**
|
|
3233
|
-
|
|
3234
|
-
```TypeScript
|
|
3235
|
-
const config = {
|
|
3236
|
-
base: 'data-',
|
|
3237
|
-
items: '${base}items',
|
|
3238
|
-
_items: {
|
|
3239
|
-
instanceOf: 'Array',
|
|
3240
|
-
parseCache: 'shared'
|
|
3241
|
-
}
|
|
3242
|
-
};
|
|
3243
|
-
|
|
3244
|
-
// HTML: <div data-items='[1,2,3]'></div>
|
|
3245
|
-
|
|
3246
|
-
const result1 = parseWithAttrs(element1, config);
|
|
3247
|
-
result1.items.push(4); // Mutation!
|
|
3248
|
-
|
|
3249
|
-
const result2 = parseWithAttrs(element2, config);
|
|
3250
|
-
console.log(result2.items); // [1,2,3,4] - mutation is visible!
|
|
3251
|
-
```
|
|
3252
|
-
|
|
3253
|
-
**Example: Cloned cache safety**
|
|
3254
|
-
|
|
3255
|
-
```TypeScript
|
|
3256
|
-
const config = {
|
|
3257
|
-
base: 'data-',
|
|
3258
|
-
items: '${base}items',
|
|
3259
|
-
_items: {
|
|
3260
|
-
instanceOf: 'Array',
|
|
3261
|
-
parseCache: 'cloned' // Safe from mutations
|
|
3262
|
-
}
|
|
3263
|
-
};
|
|
3264
|
-
|
|
3265
|
-
const result1 = parseWithAttrs(element1, config);
|
|
3266
|
-
result1.items.push(4); // Mutation
|
|
3267
|
-
|
|
3268
|
-
const result2 = parseWithAttrs(element2, config);
|
|
3269
|
-
console.log(result2.items); // [1,2,3] - original value preserved
|
|
3270
|
-
```
|
|
3271
|
-
|
|
3272
|
-
### Base Attribute
|
|
3273
|
-
|
|
3274
|
-
The special `base` property handles a single attribute that spreads into the result:
|
|
3275
|
-
|
|
3276
|
-
```TypeScript
|
|
3277
|
-
// HTML: <div data-greetings='{"hello":"world","goodbye":"Mars"}'></div>
|
|
3278
|
-
|
|
3279
|
-
const result = parseWithAttrs(element, {
|
|
3280
|
-
base: 'data-greetings'
|
|
3281
|
-
// Default: spreads into root with Object parser
|
|
3282
|
-
});
|
|
3283
|
-
// Result: { hello: 'world', goodbye: 'Mars' }
|
|
3284
|
-
|
|
3285
|
-
// With custom mapsTo:
|
|
3286
|
-
const result2 = parseWithAttrs(element, {
|
|
3287
|
-
base: 'data-greetings',
|
|
3288
|
-
_base: {
|
|
3289
|
-
mapsTo: 'greetings',
|
|
3290
|
-
instanceOf: 'Object'
|
|
3291
|
-
}
|
|
3292
|
-
});
|
|
3293
|
-
// Result: { greetings: { hello: 'world', goodbye: 'Mars' } }
|
|
3294
|
-
```
|
|
3295
|
-
|
|
3296
|
-
### Best Practices
|
|
3297
|
-
|
|
3298
|
-
1. **Use base for common prefixes**: Reduces repetition in attribute names
|
|
3299
|
-
2. **Leverage template variables**: Build complex attribute names from simple parts
|
|
3300
|
-
3. **Specify instanceOf**: Ensures proper type conversion
|
|
3301
|
-
4. **Use mapsTo for clarity**: Map attribute names to meaningful property names
|
|
3302
|
-
5. **Combine with assignGingerly**: Use nested paths (`?.`) for deep property assignment
|
|
3303
|
-
6. **Handle missing attributes**: Non-existent attributes are skipped (except Boolean types)
|
|
3304
|
-
|
|
3305
|
-
### Nested Paths with assignGingerly
|
|
3306
|
-
|
|
3307
|
-
Combine `parseWithAttrs` with `assignGingerly` for nested property assignment:
|
|
3308
|
-
|
|
3309
|
-
```TypeScript
|
|
3310
|
-
// HTML: <div data-height="100px" data--is-happy></div>
|
|
3311
|
-
|
|
3312
|
-
const element = document.createElement('div');
|
|
3313
|
-
const attrs = parseWithAttrs(element, {
|
|
3314
|
-
base: 'data-',
|
|
3315
|
-
height: '${base}height',
|
|
3316
|
-
_height: {
|
|
3317
|
-
mapsTo: '?.style?.height'
|
|
3318
|
-
},
|
|
3319
|
-
isHappy: '${base}-is-happy',
|
|
3320
|
-
_isHappy: {
|
|
3321
|
-
instanceOf: 'Boolean',
|
|
3322
|
-
mapsTo: '?.moods?.personIsHappy'
|
|
3323
|
-
}
|
|
3324
|
-
});
|
|
3325
|
-
|
|
3326
|
-
assignGingerly(element, attrs);
|
|
3327
|
-
// element.style.height === '100px'
|
|
3328
|
-
// element.moods.personIsHappy === true
|
|
3329
|
-
```
|
|
3330
|
-
|
|
3331
|
-
</details>
|
|
2453
|
+
- `parseWithAttrs` usage and automatic integration with `enh.get()` / `assignGingerly()`
|
|
2454
|
+
- The `enh-` prefix and `allowUnprefixed` rules
|
|
2455
|
+
- `AttrPatterns` / `AttrConfig` options (`mapsTo`, `instanceOf`, `parser`, `valIfNull`, `parseCache`, `sourceOfTruth`)
|
|
2456
|
+
- `buildCSSQuery` for generating CSS selectors from `withAttrs`
|
|
2457
|
+
- Using `withAttrs` with custom element features
|
|
3332
2458
|
|
|
3333
2459
|
## Building CSS Queries with `buildCSSQuery`
|
|
3334
2460
|
|
|
3335
|
-
The `buildCSSQuery`
|
|
3336
|
-
|
|
3337
|
-
### Basic Usage
|
|
3338
|
-
|
|
3339
|
-
```TypeScript
|
|
3340
|
-
import { buildCSSQuery } from 'assign-gingerly';
|
|
3341
|
-
|
|
3342
|
-
const config = {
|
|
3343
|
-
spawn: MyEnhancement,
|
|
3344
|
-
withAttrs: {
|
|
3345
|
-
base: 'my-component',
|
|
3346
|
-
theme: '${base}-theme'
|
|
3347
|
-
}
|
|
3348
|
-
};
|
|
3349
|
-
|
|
3350
|
-
const query = buildCSSQuery(config, 'div, span');
|
|
3351
|
-
console.log(query);
|
|
3352
|
-
// 'div[my-component], span[my-component], div[enh-my-component], span[enh-my-component],
|
|
3353
|
-
// div[my-component-theme], span[my-component-theme], div[enh-my-component-theme], span[enh-my-component-theme]'
|
|
3354
|
-
|
|
3355
|
-
// Use with querySelector
|
|
3356
|
-
const elements = document.querySelectorAll(query);
|
|
3357
|
-
```
|
|
3358
|
-
|
|
3359
|
-
**Without selectors (matches any element):**
|
|
3360
|
-
|
|
3361
|
-
```TypeScript
|
|
3362
|
-
// Omit the selectors parameter
|
|
3363
|
-
const query = buildCSSQuery(config);
|
|
3364
|
-
// or explicitly pass empty string
|
|
3365
|
-
const query = buildCSSQuery(config, '');
|
|
3366
|
-
|
|
3367
|
-
console.log(query);
|
|
3368
|
-
// '[my-component], [enh-my-component], [my-component-theme], [enh-my-component-theme]'
|
|
3369
|
-
|
|
3370
|
-
// Matches any element with these attributes
|
|
3371
|
-
const elements = document.querySelectorAll(query);
|
|
3372
|
-
```
|
|
3373
|
-
|
|
3374
|
-
### How It Works
|
|
3375
|
-
|
|
3376
|
-
`buildCSSQuery` creates a cross-product of:
|
|
3377
|
-
1. **Selectors**: The CSS selectors you provide (e.g., `'div, span'`)
|
|
3378
|
-
2. **Attributes**: All attribute names from `withAttrs` (resolving template variables)
|
|
3379
|
-
3. **Prefixes**: Both unprefixed and `enh-` prefixed versions
|
|
2461
|
+
The `buildCSSQuery` helper generates CSS selector strings from a `withAttrs` configuration. It is covered in [docs/withAttrs.md](docs/withAttrs.md#building-css-queries-with-buildcssquery).
|
|
3380
2462
|
|
|
3381
|
-
This ensures you find all elements that might be enhanced, regardless of whether they use the `enh-` prefix or not.
|
|
3382
|
-
|
|
3383
|
-
### Template Variable Resolution
|
|
3384
|
-
|
|
3385
|
-
Template variables in `withAttrs` are automatically resolved:
|
|
3386
|
-
|
|
3387
|
-
```TypeScript
|
|
3388
|
-
const config = {
|
|
3389
|
-
spawn: BeABeacon,
|
|
3390
|
-
withAttrs: {
|
|
3391
|
-
base: 'be-a-beacon',
|
|
3392
|
-
theme: '${base}-theme',
|
|
3393
|
-
size: '${base}-size'
|
|
3394
|
-
}
|
|
3395
|
-
};
|
|
3396
|
-
|
|
3397
|
-
buildCSSQuery(config, 'template, script');
|
|
3398
|
-
// Returns selectors for: be-a-beacon, be-a-beacon-theme, be-a-beacon-size
|
|
3399
|
-
// Each with both prefixed and unprefixed versions
|
|
3400
|
-
```
|
|
3401
|
-
|
|
3402
|
-
### Complex Selectors
|
|
3403
|
-
|
|
3404
|
-
The function supports any valid CSS selector:
|
|
3405
|
-
|
|
3406
|
-
```TypeScript
|
|
3407
|
-
const config = {
|
|
3408
|
-
spawn: MyEnhancement,
|
|
3409
|
-
withAttrs: {
|
|
3410
|
-
base: 'data-enhanced'
|
|
3411
|
-
}
|
|
3412
|
-
};
|
|
3413
|
-
|
|
3414
|
-
// Classes and IDs
|
|
3415
|
-
buildCSSQuery(config, 'div.highlight, span#special');
|
|
3416
|
-
// 'div.highlight[data-enhanced], span#special[data-enhanced], ...'
|
|
3417
|
-
|
|
3418
|
-
// Combinators
|
|
3419
|
-
buildCSSQuery(config, 'div > span, ul li');
|
|
3420
|
-
// 'div > span[data-enhanced], ul li[data-enhanced], ...'
|
|
3421
|
-
|
|
3422
|
-
// Pseudo-classes
|
|
3423
|
-
buildCSSQuery(config, 'div:hover, span:first-child');
|
|
3424
|
-
// 'div:hover[data-enhanced], span:first-child[data-enhanced], ...'
|
|
3425
|
-
|
|
3426
|
-
// Attribute selectors
|
|
3427
|
-
buildCSSQuery(config, 'div[existing-attr]');
|
|
3428
|
-
// 'div[existing-attr][data-enhanced], ...'
|
|
3429
|
-
```
|
|
3430
|
-
|
|
3431
|
-
### Underscore-Prefixed Keys Excluded
|
|
3432
|
-
|
|
3433
|
-
Configuration keys starting with `_` are excluded from the query:
|
|
3434
|
-
|
|
3435
|
-
```TypeScript
|
|
3436
|
-
const config = {
|
|
3437
|
-
spawn: MyEnhancement,
|
|
3438
|
-
withAttrs: {
|
|
3439
|
-
base: 'my-attr',
|
|
3440
|
-
_base: {
|
|
3441
|
-
mapsTo: 'something' // Config only, not an attribute
|
|
3442
|
-
},
|
|
3443
|
-
theme: '${base}-theme',
|
|
3444
|
-
_theme: {
|
|
3445
|
-
instanceOf: 'String' // Config only
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
};
|
|
3449
|
-
|
|
3450
|
-
buildCSSQuery(config, 'div');
|
|
3451
|
-
// Only includes: my-attr and my-attr-theme
|
|
3452
|
-
// Does NOT include: _base or _theme
|
|
3453
|
-
```
|
|
3454
|
-
|
|
3455
|
-
### Edge Cases
|
|
3456
|
-
|
|
3457
|
-
**Omitting or empty selectors return attribute-only selectors:**
|
|
3458
|
-
```TypeScript
|
|
3459
|
-
const config = {
|
|
3460
|
-
spawn: MyClass,
|
|
3461
|
-
withAttrs: {
|
|
3462
|
-
base: 'my-attr',
|
|
3463
|
-
theme: '${base}-theme'
|
|
3464
|
-
}
|
|
3465
|
-
};
|
|
3466
|
-
|
|
3467
|
-
buildCSSQuery(config); // Omit selectors parameter
|
|
3468
|
-
// or
|
|
3469
|
-
buildCSSQuery(config, ''); // Empty string
|
|
3470
|
-
// Both return: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
|
|
3471
|
-
// Matches any element with these attributes
|
|
3472
|
-
```
|
|
3473
|
-
|
|
3474
|
-
**Empty withAttrs returns empty string:**
|
|
3475
|
-
```TypeScript
|
|
3476
|
-
buildCSSQuery({ spawn: MyClass }, 'div'); // '' (no withAttrs)
|
|
3477
|
-
buildCSSQuery({ spawn: MyClass, withAttrs: {} }, 'div'); // '' (empty withAttrs)
|
|
3478
|
-
```
|
|
3479
|
-
|
|
3480
|
-
**Deduplication:**
|
|
3481
|
-
```TypeScript
|
|
3482
|
-
buildCSSQuery(config, 'div, div, div');
|
|
3483
|
-
// Duplicates are removed automatically
|
|
3484
|
-
```
|
|
3485
|
-
|
|
3486
|
-
**Whitespace handling:**
|
|
3487
|
-
```TypeScript
|
|
3488
|
-
buildCSSQuery(config, ' div , span , p ');
|
|
3489
|
-
// Whitespace is trimmed automatically
|
|
3490
|
-
```
|
|
3491
|
-
|
|
3492
|
-
### Use Cases
|
|
3493
|
-
|
|
3494
|
-
1. **Mount Observer Integration**: Find elements that need enhancement
|
|
3495
|
-
```TypeScript
|
|
3496
|
-
// Match any element with the attributes
|
|
3497
|
-
const matching = buildCSSQuery(enhancementConfig);
|
|
3498
|
-
const observer = new MountObserver({
|
|
3499
|
-
matching,
|
|
3500
|
-
do: (mountedElement) => {
|
|
3501
|
-
enhance(mountedElement);
|
|
3502
|
-
}
|
|
3503
|
-
});
|
|
3504
|
-
```
|
|
3505
|
-
|
|
3506
|
-
See [Mount-Observer](https://github.com/bahrus/mount-observer).
|
|
3507
|
-
|
|
3508
|
-
2. **Specific Element Types**: Enhance only certain element types
|
|
3509
|
-
```TypeScript
|
|
3510
|
-
const query = buildCSSQuery(config, 'template, script');
|
|
3511
|
-
document.querySelectorAll(query).forEach(el => {
|
|
3512
|
-
const instance = el.enh.get(config);
|
|
3513
|
-
});
|
|
3514
|
-
```
|
|
3515
|
-
|
|
3516
|
-
3. **Conditional Enhancement**: Find elements in specific contexts
|
|
3517
|
-
```TypeScript
|
|
3518
|
-
const query = buildCSSQuery(config, '.container > div');
|
|
3519
|
-
const elements = document.querySelectorAll(query);
|
|
3520
|
-
```
|
|
3521
|
-
|
|
3522
|
-
### API Reference
|
|
3523
|
-
|
|
3524
|
-
```TypeScript
|
|
3525
|
-
function buildCSSQuery(
|
|
3526
|
-
config: EnhancementConfig,
|
|
3527
|
-
selectors?: string
|
|
3528
|
-
): string
|
|
3529
|
-
```
|
|
3530
|
-
|
|
3531
|
-
**Parameters:**
|
|
3532
|
-
- `config`: Enhancement configuration with `withAttrs` property
|
|
3533
|
-
- `selectors` (optional): Comma-separated CSS selectors (e.g., `'div, span'`)
|
|
3534
|
-
- If omitted or empty string, returns attribute selectors without element prefix
|
|
3535
|
-
- This matches any element with the specified attributes
|
|
3536
|
-
|
|
3537
|
-
**Returns:**
|
|
3538
|
-
- CSS query string with cross-product of selectors and attributes
|
|
3539
|
-
- If selectors is omitted or empty: returns attribute-only selectors (e.g., `'[attr], [enh-attr]'`)
|
|
3540
|
-
- If withAttrs is missing or empty: returns empty string
|
|
3541
|
-
|
|
3542
|
-
**Throws:**
|
|
3543
|
-
- Error if template variables have circular references
|
|
3544
|
-
- Error if template variables reference undefined keys
|
|
3545
|
-
|
|
3546
|
-
### Performance Notes
|
|
3547
|
-
|
|
3548
|
-
- The function is synchronous and fast
|
|
3549
|
-
- Resulting queries can be long with many attributes, but CSS engines handle this efficiently
|
|
3550
|
-
- Queries are deduplicated automatically
|
|
3551
|
-
- Consider caching the result if calling repeatedly with the same config
|
|
3552
|
-
|
|
3553
|
-
<!--
|
|
3554
|
-
|
|
3555
|
-
### Complete Example
|
|
3556
|
-
|
|
3557
|
-
```TypeScript
|
|
3558
|
-
// HTML: <user-card
|
|
3559
|
-
// data-config='{"theme":"dark"}'
|
|
3560
|
-
// data-config-name="Alice"
|
|
3561
|
-
// data-config-age="30"
|
|
3562
|
-
// data-config-active
|
|
3563
|
-
// ></user-card>
|
|
3564
|
-
|
|
3565
|
-
const element = document.querySelector('user-card');
|
|
3566
|
-
const result = parseWithAttrs(element, {
|
|
3567
|
-
base: 'data-config',
|
|
3568
|
-
_base: {
|
|
3569
|
-
mapsTo: 'settings',
|
|
3570
|
-
instanceOf: 'Object'
|
|
3571
|
-
},
|
|
3572
|
-
name: '${base}-name',
|
|
3573
|
-
age: '${base}-age',
|
|
3574
|
-
_age: {
|
|
3575
|
-
instanceOf: 'Number',
|
|
3576
|
-
mapsTo: 'userAge'
|
|
3577
|
-
},
|
|
3578
|
-
active: '${base}-active',
|
|
3579
|
-
_active: {
|
|
3580
|
-
instanceOf: 'Boolean',
|
|
3581
|
-
mapsTo: 'isActive'
|
|
3582
|
-
}
|
|
3583
|
-
});
|
|
3584
|
-
|
|
3585
|
-
console.log(result);
|
|
3586
|
-
// {
|
|
3587
|
-
// settings: { theme: 'dark' },
|
|
3588
|
-
// name: 'Alice',
|
|
3589
|
-
// userAge: 30,
|
|
3590
|
-
// isActive: true
|
|
3591
|
-
// }
|
|
3592
|
-
```
|
|
3593
|
-
|
|
3594
|
-
-->
|
|
3595
2463
|
|
|
3596
2464
|
## Resolving and Assigning with `assignFrom`
|
|
3597
2465
|
|
|
@@ -3824,16 +2692,18 @@ await assignFromAsync(myElement, {
|
|
|
3824
2692
|
|
|
3825
2693
|
Import paths must be local (relative, absolute, or bare specifiers — no cross-domain URLs). The module's default export is checked first; otherwise the first exported class with an `assign` method is used.
|
|
3826
2694
|
|
|
3827
|
-
Built-in handlers (`builtIns.lazyLoad`, `builtIns.
|
|
2695
|
+
Built-in handlers (`builtIns.lazyLoad`, `builtIns.microDataJoin`, etc.) auto-load without needing to be listed in `handlers`.
|
|
2696
|
+
|
|
2697
|
+
> `join` isn't in this list — it's a synchronous [` =&` op](#sync-op-join) now, not an async `do:` handler, so it's never dynamically imported and never awaited.
|
|
3828
2698
|
|
|
3829
2699
|
**Handler aliases:** The `handlers` option also accepts built-in names as values, allowing you to define short aliases (including emoji) for concise configs:
|
|
3830
2700
|
|
|
3831
2701
|
```JavaScript
|
|
3832
2702
|
import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
3833
|
-
// { '📦': 'builtIns.lazyLoad', '🎚️': 'builtIns.lazyLoadSwitch', '
|
|
2703
|
+
// { '📦': 'builtIns.lazyLoad', '🎚️': 'builtIns.lazyLoadSwitch', '🏷️': 'builtIns.microDataJoin', '📋': 'builtIns.manageTemplateList' }
|
|
3834
2704
|
|
|
3835
2705
|
assignFrom(target, {
|
|
3836
|
-
'?.
|
|
2706
|
+
'?.el =>': { do: '🏷️', get: { template: [...] } }
|
|
3837
2707
|
}, { from: vm, handlers: builtInEmoji });
|
|
3838
2708
|
|
|
3839
2709
|
// Or define your own:
|
|
@@ -4093,9 +2963,15 @@ await assignFromAsync(document.body, {
|
|
|
4093
2963
|
- Mixed `do` values — fully supported, each handler is looked up independently.
|
|
4094
2964
|
- Error handling — fail-fast. If a handler throws, remaining handlers are skipped.
|
|
4095
2965
|
|
|
4096
|
-
|
|
2966
|
+
**Return-value protocol:**
|
|
2967
|
+
|
|
2968
|
+
When a handler's `assign()` method returns a non-`undefined` value, `processHandlerCommands` assigns it back to the LHS path. Handlers like `builtIns.lazyLoad` return `undefined` (void) and operate by side effects instead.
|
|
2969
|
+
|
|
2970
|
+
### Sync op: join (` =&`)
|
|
4097
2971
|
|
|
4098
|
-
|
|
2972
|
+
` =&` is a separate operator from ` =>`, for computed values that have nothing to await — no dynamic import, no handler class, no microtask hop, ever. The RHS names exactly one op from a small built-in registry (`syncOps/registry.ts`); sibling keys are that op's own config. `join` is the first (and so far only) op in the registry.
|
|
2973
|
+
|
|
2974
|
+
`join` resolves an array of `?.` paths and literals, then joins it into a single string. It supports nested sub-arrays with "all-or-nothing" semantics for optional segments.
|
|
4099
2975
|
|
|
4100
2976
|
```JavaScript
|
|
4101
2977
|
const vm = {
|
|
@@ -4104,11 +2980,8 @@ const vm = {
|
|
|
4104
2980
|
};
|
|
4105
2981
|
|
|
4106
2982
|
assignFrom(oElement, {
|
|
4107
|
-
'?.textContent
|
|
4108
|
-
|
|
4109
|
-
get: {
|
|
4110
|
-
value: ['?.lastName', ', ', '?.firstName']
|
|
4111
|
-
}
|
|
2983
|
+
'?.textContent =&': {
|
|
2984
|
+
join: ['?.lastName', ', ', '?.firstName']
|
|
4112
2985
|
}
|
|
4113
2986
|
}, { from: vm });
|
|
4114
2987
|
|
|
@@ -4117,11 +2990,11 @@ assignFrom(oElement, {
|
|
|
4117
2990
|
|
|
4118
2991
|
**How it works:**
|
|
4119
2992
|
|
|
4120
|
-
1. The `
|
|
4121
|
-
2. Top-level `null`/`undefined` values are filtered out.
|
|
2993
|
+
1. The whole config object (`{ join: [...], separator: '...' }`) is resolved in one pass by `getValues` — `?.` path strings anywhere inside it, including nested inside the `join` array, are replaced with actual values from `options.from`.
|
|
2994
|
+
2. Top-level `null`/`undefined` values in the resolved array are filtered out.
|
|
4122
2995
|
3. Nested sub-arrays use **all-or-nothing** semantics: if any element in a sub-array resolves to `null`/`undefined`, the entire sub-array is dropped.
|
|
4123
|
-
4. Remaining elements are joined with
|
|
4124
|
-
5. The joined string is
|
|
2996
|
+
4. Remaining elements are joined with `separator` (default: `''`, empty string).
|
|
2997
|
+
5. The joined string is assigned straight to the LHS path — every sync op always produces a value to assign; there's no opt-in return-value protocol to it like ` =>` has.
|
|
4125
2998
|
|
|
4126
2999
|
**Optional segments with nested arrays:**
|
|
4127
3000
|
|
|
@@ -4133,11 +3006,8 @@ const vm = {
|
|
|
4133
3006
|
};
|
|
4134
3007
|
|
|
4135
3008
|
assignFrom(oElement, {
|
|
4136
|
-
'?.textContent
|
|
4137
|
-
|
|
4138
|
-
get: {
|
|
4139
|
-
value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
4140
|
-
}
|
|
3009
|
+
'?.textContent =&': {
|
|
3010
|
+
join: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
4141
3011
|
}
|
|
4142
3012
|
}, { from: vm });
|
|
4143
3013
|
|
|
@@ -4152,21 +3022,16 @@ assignFrom(oElement, {
|
|
|
4152
3022
|
|
|
4153
3023
|
```JavaScript
|
|
4154
3024
|
assignFrom(oElement, {
|
|
4155
|
-
'?.textContent
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
value: ['?.firstName', '?.lastName'],
|
|
4159
|
-
separator: ' | '
|
|
4160
|
-
}
|
|
3025
|
+
'?.textContent =&': {
|
|
3026
|
+
join: ['?.firstName', '?.lastName'],
|
|
3027
|
+
separator: ' | '
|
|
4161
3028
|
}
|
|
4162
3029
|
}, { from: vm });
|
|
4163
3030
|
|
|
4164
3031
|
// oElement.textContent = 'Helaena | Targaryen'
|
|
4165
3032
|
```
|
|
4166
3033
|
|
|
4167
|
-
**
|
|
4168
|
-
|
|
4169
|
-
When a handler's `assign()` method returns a non-`undefined` value, `processHandlerCommands` assigns it back to the LHS path. This is how `builtIns.join` sets `textContent` — the handler computes the string and returns it. Existing handlers like `builtIns.lazyLoad` return `undefined` (void) and operate by side effects, so they're unaffected.
|
|
3034
|
+
**On `options.protocols`:** `getValues` runs synchronously and calls protocol handlers without awaiting them. `options.protocols` is typed to allow an async handler (`(key: string) => any | Promise<any>`), because ` =>`'s `resolve:` path can use one — but ` =&` has no await to catch a Promise coming back from one. If a resolved value under ` =&` turns out to be a thenable, assignFrom throws immediately rather than silently assigning `"[object Promise]"`. Use ` =>` with `resolve:` for handler configs that need an async protocol.
|
|
4170
3035
|
|
|
4171
3036
|
### Built-in handler: `builtIns.microDataJoin`
|
|
4172
3037
|
|
|
@@ -4228,7 +3093,7 @@ Produces:
|
|
|
4228
3093
|
|
|
4229
3094
|
**Optional segments (nested arrays):**
|
|
4230
3095
|
|
|
4231
|
-
Same all-or-nothing semantics as `
|
|
3096
|
+
Same all-or-nothing semantics as the [`join` sync op](#sync-op-join) — if any `val` in a nested sub-array is null/undefined, the entire sub-array is dropped:
|
|
4232
3097
|
|
|
4233
3098
|
```JavaScript
|
|
4234
3099
|
get: {
|
|
@@ -4372,7 +3237,7 @@ assignFrom(element, {
|
|
|
4372
3237
|
|
|
4373
3238
|
## Typed Path Authoring with `paths`, `sp`, and `md`
|
|
4374
3239
|
|
|
4375
|
-
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for `
|
|
3240
|
+
For JSON generated config files generated from TypeScript/`.mts`/`mjs` files during a build or server-side rendering, the `paths` utility provides compile-time autocomplete and type safety for `?.`-prefixed path strings. The `sp` tagged template literal ("split into parts") produces arrays suitable for the [`join` sync op](#sync-op-join). The `md` tagged template literal produces `{prop, val}` objects suitable for `builtIns.microDataJoin`.
|
|
4376
3241
|
|
|
4377
3242
|
```TypeScript
|
|
4378
3243
|
import { paths, sp } from 'assign-gingerly/DX/paths.js';
|
|
@@ -4389,11 +3254,8 @@ const $ = paths<Person>();
|
|
|
4389
3254
|
// sp produces: ['?.lastName', ', ', '?.firstName']
|
|
4390
3255
|
// with full autocomplete on $.lastName, $.firstName, etc.
|
|
4391
3256
|
export default {
|
|
4392
|
-
'?.textContent
|
|
4393
|
-
|
|
4394
|
-
get: {
|
|
4395
|
-
value: sp`${$.lastName}, ${$.firstName}`
|
|
4396
|
-
}
|
|
3257
|
+
'?.textContent =&': {
|
|
3258
|
+
join: sp`${$.lastName}, ${$.firstName}`
|
|
4397
3259
|
}
|
|
4398
3260
|
};
|
|
4399
3261
|
```
|
|
@@ -4401,9 +3263,9 @@ export default {
|
|
|
4401
3263
|
**How `paths` works:**
|
|
4402
3264
|
|
|
4403
3265
|
- `paths<T>()` creates a deeply-proxied object typed as `T`
|
|
4404
|
-
- Every property access returns a deeper proxy (e.g., `$.address.city`)
|
|
3266
|
+
- Every property access returns a deeper proxy (e.g., `$.address.city`)
|
|
4405
3267
|
- Capitalized reserved tokens like `.Each` and `.EqNot` normalize into command syntax
|
|
4406
|
-
- `.Path` extracts the `?.`-prefixed string: `$.address.city.Path` → `'?.address?.city'`
|
|
3268
|
+
- `.Path` extracts the `?.`-prefixed string: `$.address.city.Path` → `'?.address?.city'`
|
|
4407
3269
|
- Inside `sp` template literals, `.Path` is not needed — proxy objects are auto-detected
|
|
4408
3270
|
|
|
4409
3271
|
**How `sp` works:**
|
|
@@ -4472,7 +3334,7 @@ md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
|
|
|
4472
3334
|
|
|
4473
3335
|
| Tag | Output for proxy interpolation | Use with |
|
|
4474
3336
|
|-----|-------------------------------|----------|
|
|
4475
|
-
| `sp` | `'?.firstName'` (path string) | `
|
|
3337
|
+
| `sp` | `'?.firstName'` (path string) | the `join` sync op |
|
|
4476
3338
|
| `md` | `{ prop: 'firstName', val: '?.firstName' }` | `builtIns.microDataJoin` |
|
|
4477
3339
|
|
|
4478
3340
|
Both auto-detect path proxies (no `.Path` needed inside template literals) and preserve nested arrays for optional segments.
|
|
@@ -5763,43 +4625,9 @@ Because the real class is resolved before the getter is installed, the first acc
|
|
|
5763
4625
|
|
|
5764
4626
|
### Attribute parsing with `withAttrs`
|
|
5765
4627
|
|
|
5766
|
-
Features can declare attribute patterns
|
|
5767
|
-
|
|
5768
|
-
```JavaScript
|
|
5769
|
-
customElements.assignFeatures(ClubMember, {
|
|
5770
|
-
photoTaker: {
|
|
5771
|
-
spawn: PhotoTakerImpl,
|
|
5772
|
-
withAttrs: {
|
|
5773
|
-
base: 'photo',
|
|
5774
|
-
resolution: '${base}-resolution',
|
|
5775
|
-
format: '${base}-format'
|
|
5776
|
-
}
|
|
5777
|
-
}
|
|
5778
|
-
});
|
|
5779
|
-
```
|
|
5780
|
-
|
|
5781
|
-
```HTML
|
|
5782
|
-
<club-member photo-resolution="4k" photo-format="png"></club-member>
|
|
5783
|
-
```
|
|
5784
|
-
|
|
5785
|
-
This parses into `initVals = { resolution: '4k', format: 'png' }`. By default, non-underscore keys are assumed to be strings with `mapsTo` equal to the key name. The `_key` form is only needed to override defaults (e.g., parse as Number, map to a different property name, use a custom parser):
|
|
5786
|
-
|
|
5787
|
-
```JavaScript
|
|
5788
|
-
withAttrs: {
|
|
5789
|
-
base: 'photo',
|
|
5790
|
-
resolution: '${base}-resolution',
|
|
5791
|
-
// Override: parse as Number instead of String
|
|
5792
|
-
_resolution: { instanceOf: 'Number', mapsTo: 'resolutionPx' },
|
|
5793
|
-
format: '${base}-format'
|
|
5794
|
-
// No _format needed — defaults to String, mapsTo: 'format'
|
|
5795
|
-
}
|
|
5796
|
-
```
|
|
5797
|
-
|
|
5798
|
-
**Merge priority (lowest to highest):**
|
|
5799
|
-
1. Attribute-parsed values (`withAttrs`)
|
|
5800
|
-
2. Programmatic `initVals` (from `captureFeatureInitVals`)
|
|
4628
|
+
Features can declare attribute patterns that are parsed from the host element into `initVals`. Unlike enhancements, feature attributes are always read unprefixed. Programmatic `initVals` take precedence over parsed values.
|
|
5801
4629
|
|
|
5802
|
-
|
|
4630
|
+
See [docs/withAttrs.md](docs/withAttrs.md#attribute-patterns-for-custom-element-features) for the full guide and examples.
|
|
5803
4631
|
|
|
5804
4632
|
### Shared context with `getSharedContext`
|
|
5805
4633
|
|