assign-gingerly 0.0.84 → 0.0.85
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 +12 -1179
- package/ScopedParserRegistry.js +2 -2
- package/ScopedParserRegistry.ts +6 -6
- package/SplitParser.js +48 -0
- package/SplitParser.ts +89 -0
- package/inferencer/types/NewHTMLFirstCustomElement.md +66 -1
- package/package.json +5 -1
- package/parseWithAttrs.js +109 -25
- package/parseWithAttrs.ts +135 -31
- package/parserRegistry.js +5 -4
- package/parserRegistry.ts +10 -12
- package/types/assign-gingerly/types.d.ts +43 -5
package/README.md
CHANGED
|
@@ -1641,7 +1641,7 @@ interface EnhancementConfig<T, TObj = Element> {
|
|
|
1641
1641
|
}
|
|
1642
1642
|
```
|
|
1643
1643
|
|
|
1644
|
-
The `withAttrs` property enables automatic attribute parsing when the enhancement is spawned. See
|
|
1644
|
+
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
1645
|
|
|
1646
1646
|
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
1647
|
|
|
@@ -2443,1155 +2443,22 @@ assignGingerly(element, { [enhSymbol]: 'test' }, { registry });
|
|
|
2443
2443
|
|
|
2444
2444
|
</details>
|
|
2445
2445
|
|
|
2446
|
-
## Parsing Attributes with `
|
|
2446
|
+
## Parsing HTML Attributes with `withAttrs`
|
|
2447
2447
|
|
|
2448
|
-
|
|
2448
|
+
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
2449
|
|
|
2450
|
-
|
|
2450
|
+
See [docs/withAttrs.md](docs/withAttrs.md) for the full reference, including:
|
|
2451
2451
|
|
|
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>
|
|
2452
|
+
- `parseWithAttrs` usage and automatic integration with `enh.get()` / `assignGingerly()`
|
|
2453
|
+
- The `enh-` prefix and `allowUnprefixed` rules
|
|
2454
|
+
- `AttrPatterns` / `AttrConfig` options (`mapsTo`, `instanceOf`, `parser`, `valIfNull`, `parseCache`, `sourceOfTruth`)
|
|
2455
|
+
- `buildCSSQuery` for generating CSS selectors from `withAttrs`
|
|
2456
|
+
- Using `withAttrs` with custom element features
|
|
3332
2457
|
|
|
3333
2458
|
## Building CSS Queries with `buildCSSQuery`
|
|
3334
2459
|
|
|
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
|
|
3380
|
-
|
|
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], ...'
|
|
2460
|
+
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).
|
|
3421
2461
|
|
|
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
2462
|
|
|
3596
2463
|
## Resolving and Assigning with `assignFrom`
|
|
3597
2464
|
|
|
@@ -5763,43 +4630,9 @@ Because the real class is resolved before the getter is installed, the first acc
|
|
|
5763
4630
|
|
|
5764
4631
|
### Attribute parsing with `withAttrs`
|
|
5765
4632
|
|
|
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`)
|
|
4633
|
+
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
4634
|
|
|
5802
|
-
|
|
4635
|
+
See [docs/withAttrs.md](docs/withAttrs.md#attribute-patterns-for-custom-element-features) for the full guide and examples.
|
|
5803
4636
|
|
|
5804
4637
|
### Shared context with `getSharedContext`
|
|
5805
4638
|
|