assign-gingerly 0.0.22 → 0.0.23
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 +460 -23
- package/assignGingerly.js +118 -1
- package/assignGingerly.ts +160 -1
- package/handleIshProperty.js +92 -0
- package/handleIshProperty.ts +115 -0
- package/index.js +1 -1
- package/index.ts +1 -1
- package/object-extension.js +20 -1
- package/object-extension.ts +23 -2
- package/package.json +1 -1
- package/parseWithAttrs.js +29 -23
- package/parseWithAttrs.ts +41 -24
- package/playwright.config.ts +9 -9
- package/types/assign-gingerly/types.d.ts +58 -6
- package/types/global.d.ts +4 -0
package/README.md
CHANGED
|
@@ -23,13 +23,17 @@ One can achieve the same functionality with a little more work, and "playing nic
|
|
|
23
23
|
|
|
24
24
|
Not only does this polyfill package allow merging data properties onto objects that are expecting them, this polyfill also provides the ability to merge *augmented behavior* onto run-time objects without sub classing all such objects of the same type. This includes the ability to spawn an instance of a class and "merge" it into the API of the original object in an elegant way that is easy to wrap one's brain around, without ever blocking access to the original object or breaking it.
|
|
25
25
|
|
|
26
|
+
So we are providing a form of the ["Decorator Pattern"](https://en.wikipedia.org/wiki/Decorator_pattern) or perhaps more accurately the [Extension Object Pattern](https://swiftorial.com/swiftlessons/design-patterns/structural-patterns/extension-object-pattern) as tailored for the quirks of the web.
|
|
26
27
|
|
|
28
|
+
## Custom Registries
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
On top of that, this polyfill package builds on the newly minted Custom Element Registry, adding additional sub-registries:
|
|
31
|
+
|
|
32
|
+
1. [enhancementRegistry](#enhancement-registry-addendum-to-the-custom-element-registry) object on top of the customElementRegistry object associated with all elements, to be able to lazy load object extensions on demand while avoiding namespace conflicts, and, importantly, as a basis for defining custom attributes associated with the enhancements.
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
2. [itemscopeRegistry for Itemscope Managers](#itemscoperegistry) to automatically associate a function prototype or class instance with the itemscope attribute of an HTMLElement.
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
3. Custom Element Features [TODO]
|
|
33
37
|
|
|
34
38
|
So in our view this package helps fill the void left by not supporting the "is" attribute for built-in elements (but is not a complete solution, just a critical building block). Mount-observer, mount-observer-script-element, and custom enhancements builds on top of the critical role that assign-gingerly plays.
|
|
35
39
|
|
|
@@ -296,7 +300,7 @@ interface IEnhancementRegistryItem<T = any, TObjToExtend = any> {
|
|
|
296
300
|
spawn: {new(objToExtend: TObjToExtend, ctx: SpawnContext, initVals: Partial<T>): T}
|
|
297
301
|
symlinks?: {[key: symbol]: keyof T}
|
|
298
302
|
// Optional: for element enhancement access
|
|
299
|
-
enhKey?: string
|
|
303
|
+
enhKey?: string | symbol
|
|
300
304
|
// Optional: automatic attribute parsing
|
|
301
305
|
withAttrs?: AttrPatterns<T>
|
|
302
306
|
}
|
|
@@ -332,7 +336,7 @@ EnhancementRegistry.push([
|
|
|
332
336
|
},
|
|
333
337
|
spawn: MyEnhancement,
|
|
334
338
|
},{
|
|
335
|
-
|
|
339
|
+
enhKey: 'mellowYellow',
|
|
336
340
|
symlinks: {
|
|
337
341
|
[isMellow]: 'isMellow'
|
|
338
342
|
},
|
|
@@ -511,12 +515,16 @@ The prototype extensions are non-enumerable and won't appear in `Object.keys()`
|
|
|
511
515
|
|
|
512
516
|
-->
|
|
513
517
|
|
|
514
|
-
## Custom Element Registry
|
|
518
|
+
## Enhancement Registry Addendum to the Custom Element Registry
|
|
515
519
|
|
|
516
|
-
This package
|
|
520
|
+
This package polyfill adds an "enhancementRegistry" registry on the CustomElementRegistry prototype.
|
|
521
|
+
|
|
522
|
+
In this way, we achieve dependency injection in harmony with scoped custom elements DOM registry scope islands.
|
|
517
523
|
|
|
518
524
|
> [!NOTE]
|
|
519
|
-
> Safari/WebKit played a critical role in pushing scoped custom element registries forward, and announced with little fanfare or documentation that [Safari 26 supports it](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes). However, the Playwright test machinery's cross platform Safari test browser doesn't yet support it.
|
|
525
|
+
> Safari/WebKit played a critical role in pushing scoped custom element registries forward, and announced with little fanfare or documentation that [Safari 26 supports it](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes). However, the Playwright test machinery's cross platform Safari test browser doesn't yet support it. For now, only Chrome 146+ has been tested / vetted for this functionality.
|
|
526
|
+
>
|
|
527
|
+
> For more information about scoped custom element registries, see [Chrome's announcement and guide](https://developer.chrome.com/blog/scoped-registries).
|
|
520
528
|
|
|
521
529
|
<details>
|
|
522
530
|
<summary>Automatic Registry Population</summary>
|
|
@@ -680,7 +688,7 @@ class Enhancement<T> {
|
|
|
680
688
|
|
|
681
689
|
All parameters are optional for backward compatibility with existing code.
|
|
682
690
|
|
|
683
|
-
Note that the class need not extend any base class or leverage any mixins. In fact, ES5 prototype functions can be used, and in both cases are
|
|
691
|
+
Note that the class need not extend any base class or leverage any mixins. In fact, ES5 prototype functions can be used, and in both cases are instantiated using new .... Arrow functions cannot be used.
|
|
684
692
|
|
|
685
693
|
<details>
|
|
686
694
|
<summary>Passing Custom Context</summary>
|
|
@@ -1652,7 +1660,10 @@ interface AttrPatterns<T> {
|
|
|
1652
1660
|
interface AttrConfig<T> {
|
|
1653
1661
|
mapsTo?: keyof T | '.'; // Target property name (or '.' to spread)
|
|
1654
1662
|
instanceOf?: string | Function; // Type for default parser
|
|
1655
|
-
parser?:
|
|
1663
|
+
parser?:
|
|
1664
|
+
| ((v: string | null) => any) // Inline parser function
|
|
1665
|
+
| string // Named parser from globalParserRegistry
|
|
1666
|
+
| [string, string]; // [CustomElementName, StaticMethodName]
|
|
1656
1667
|
}
|
|
1657
1668
|
```
|
|
1658
1669
|
|
|
@@ -1772,7 +1783,7 @@ The following parsers are pre-registered in `globalParserRegistry`:
|
|
|
1772
1783
|
|
|
1773
1784
|
**Custom Element Static Method Parsers:**
|
|
1774
1785
|
|
|
1775
|
-
You can
|
|
1786
|
+
You can reference static methods on custom elements using tuple syntax `[elementName, methodName]`:
|
|
1776
1787
|
|
|
1777
1788
|
```TypeScript
|
|
1778
1789
|
class MyWidget extends HTMLElement {
|
|
@@ -1786,29 +1797,47 @@ class MyWidget extends HTMLElement {
|
|
|
1786
1797
|
}
|
|
1787
1798
|
customElements.define('my-widget', MyWidget);
|
|
1788
1799
|
|
|
1789
|
-
// Reference custom element parsers
|
|
1800
|
+
// Reference custom element parsers using tuple syntax
|
|
1790
1801
|
const config = {
|
|
1791
1802
|
base: 'data-',
|
|
1792
1803
|
value: '${base}value',
|
|
1793
1804
|
_value: {
|
|
1794
|
-
parser: 'my-widget
|
|
1805
|
+
parser: ['my-widget', 'parseSpecialFormat'] // [element-name, methodName]
|
|
1806
|
+
},
|
|
1807
|
+
title: '${base}title',
|
|
1808
|
+
_title: {
|
|
1809
|
+
parser: ['my-widget', 'parseWithPrefix']
|
|
1795
1810
|
}
|
|
1796
1811
|
};
|
|
1812
|
+
|
|
1813
|
+
const result = parseWithAttrs(element, config);
|
|
1797
1814
|
```
|
|
1798
1815
|
|
|
1799
|
-
**Parser Resolution
|
|
1816
|
+
**Parser Resolution:**
|
|
1817
|
+
|
|
1818
|
+
When a parser is specified, it can be:
|
|
1819
|
+
|
|
1820
|
+
1. **Inline function** - `parser: (v) => v.toUpperCase()` - Used directly
|
|
1821
|
+
2. **String reference** - `parser: 'timestamp'` - Looks up in `globalParserRegistry`
|
|
1822
|
+
3. **Tuple reference** - `parser: ['my-widget', 'parseMethod']` - Looks up static method on custom element constructor
|
|
1823
|
+
|
|
1824
|
+
**Error Handling:**
|
|
1800
1825
|
|
|
1801
|
-
|
|
1826
|
+
The tuple syntax provides clear error messages:
|
|
1802
1827
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1828
|
+
```TypeScript
|
|
1829
|
+
// Element not found
|
|
1830
|
+
parser: ['non-existent', 'method']
|
|
1831
|
+
// Error: Cannot resolve parser [non-existent, method]: custom element "non-existent" not found
|
|
1832
|
+
|
|
1833
|
+
// Method not found
|
|
1834
|
+
parser: ['my-widget', 'nonExistent']
|
|
1835
|
+
// Error: Cannot resolve parser [my-widget, nonExistent]: static method "nonExistent" not found on custom element "my-widget"
|
|
1807
1836
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1837
|
+
// String not found in registry
|
|
1838
|
+
parser: 'unknown'
|
|
1839
|
+
// Error: Parser "unknown" not found in globalParserRegistry. If you want to reference a custom element static method, use tuple syntax: ["element-name", "methodName"]
|
|
1840
|
+
```
|
|
1812
1841
|
|
|
1813
1842
|
**Example: Organizing Parsers**
|
|
1814
1843
|
|
|
@@ -2452,6 +2481,414 @@ console.log(result);
|
|
|
2452
2481
|
|
|
2453
2482
|
-->
|
|
2454
2483
|
|
|
2484
|
+
## Itemscope Managers (Chrome 146+)
|
|
2485
|
+
|
|
2486
|
+
Itemscope Managers provide a way to manage DOM fragments and their associated data/view models for elements with the `itemscope` attribute. This feature enables frameworks and libraries to manage light children of web components, DOM fragments from looping constructs, and scenarios where custom element wrapping is not feasible.
|
|
2487
|
+
|
|
2488
|
+
> [!NOTE]
|
|
2489
|
+
> This feature requires Chrome 146+ with scoped custom element registry support. It follows the same browser support requirements as the Enhancement Registry integration.
|
|
2490
|
+
>
|
|
2491
|
+
> For more information about scoped custom element registries, see [Chrome's announcement and guide](https://developer.chrome.com/blog/scoped-registries).
|
|
2492
|
+
|
|
2493
|
+
### Why Itemscope Managers?
|
|
2494
|
+
|
|
2495
|
+
The `itemscope` attribute (from the Microdata specification) provides a semantic way to mark elements that represent distinct data items. ItemScope Managers build on this by allowing us to:
|
|
2496
|
+
|
|
2497
|
+
- **Manage light children**: Attach behavior to light DOM children of web components without wrapping them in custom elements
|
|
2498
|
+
- **Handle template loops**: Manage repeated DOM fragments generated by template systems
|
|
2499
|
+
- **Avoid custom element overhead**: Enhance elements where custom element registration isn't appropriate or possible
|
|
2500
|
+
- **Separate concerns**: Keep data/view model logic separate from the DOM structure
|
|
2501
|
+
|
|
2502
|
+
### Basic Usage
|
|
2503
|
+
|
|
2504
|
+
```html
|
|
2505
|
+
<div itemscope="user-card">
|
|
2506
|
+
<h2>User Profile</h2>
|
|
2507
|
+
<p itemprop="name"></p>
|
|
2508
|
+
<p itemprop="email"></p>
|
|
2509
|
+
</div>
|
|
2510
|
+
```
|
|
2511
|
+
|
|
2512
|
+
```TypeScript
|
|
2513
|
+
import 'assign-gingerly/object-extension.js';
|
|
2514
|
+
|
|
2515
|
+
// Define a manager class
|
|
2516
|
+
class UserCardManager {
|
|
2517
|
+
element;
|
|
2518
|
+
name = '';
|
|
2519
|
+
email = '';
|
|
2520
|
+
|
|
2521
|
+
constructor(element, initVals) {
|
|
2522
|
+
this.element = element;
|
|
2523
|
+
if (initVals) {
|
|
2524
|
+
Object.assign(this, initVals);
|
|
2525
|
+
this.render();
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
render() {
|
|
2530
|
+
this.element.querySelector('[itemprop="name"]').textContent = this.name;
|
|
2531
|
+
this.element.querySelector('[itemprop="email"]').textContent = this.email;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Register the manager
|
|
2536
|
+
customElements.itemscopeRegistry.define('user-card', {
|
|
2537
|
+
manager: UserCardManager
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
// Use assignGingerly with the 'ish' property
|
|
2541
|
+
const element = document.querySelector('[itemscope="user-card"]');
|
|
2542
|
+
element.assignGingerly({
|
|
2543
|
+
ish: {
|
|
2544
|
+
name: 'Alice',
|
|
2545
|
+
email: 'alice@example.com'
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
// Wait for async setup to complete
|
|
2550
|
+
await customElements.itemscopeRegistry.whenDefined('user-card');
|
|
2551
|
+
|
|
2552
|
+
// Access the manager instance
|
|
2553
|
+
console.log(element.ish instanceof UserCardManager); // true
|
|
2554
|
+
console.log(element.ish.name); // 'Alice'
|
|
2555
|
+
```
|
|
2556
|
+
|
|
2557
|
+
### The 'ish' Property
|
|
2558
|
+
|
|
2559
|
+
The `ish` property (short for "itemscope host") is the key to ItemScope Managers:
|
|
2560
|
+
|
|
2561
|
+
- **Special behavior for HTMLElements**: When you assign an `ish` property to an HTMLElement with an `itemscope` attribute, it triggers manager instantiation
|
|
2562
|
+
- **Normal property for other objects**: For non-HTMLElement objects, `ish` is just a regular property with no special behavior
|
|
2563
|
+
- **Asynchronous setup**: The manager is instantiated asynchronously, so use `whenDefined()` to wait for completion
|
|
2564
|
+
|
|
2565
|
+
```TypeScript
|
|
2566
|
+
// HTMLElement with itemscope - special behavior
|
|
2567
|
+
const div = document.createElement('div');
|
|
2568
|
+
div.setAttribute('itemscope', 'my-manager');
|
|
2569
|
+
div.assignGingerly({ ish: { prop: 'value' } });
|
|
2570
|
+
// Manager will be instantiated asynchronously
|
|
2571
|
+
|
|
2572
|
+
// Plain object - normal property
|
|
2573
|
+
const obj = {};
|
|
2574
|
+
obj.assignGingerly({ ish: { prop: 'value' } });
|
|
2575
|
+
console.log(obj.ish.prop); // 'value' - just a regular property
|
|
2576
|
+
```
|
|
2577
|
+
|
|
2578
|
+
### ItemscopeRegistry
|
|
2579
|
+
|
|
2580
|
+
The `ItemscopeRegistry` class manages manager configurations and extends `EventTarget` to support lazy registration:
|
|
2581
|
+
|
|
2582
|
+
```TypeScript
|
|
2583
|
+
// Access the global registry
|
|
2584
|
+
const registry = customElements.itemscopeRegistry;
|
|
2585
|
+
|
|
2586
|
+
// Define a manager
|
|
2587
|
+
registry.define('manager-name', {
|
|
2588
|
+
manager: ManagerClass,
|
|
2589
|
+
lifecycleKeys: {
|
|
2590
|
+
dispose: 'cleanup',
|
|
2591
|
+
resolved: 'isReady'
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
// Get a manager configuration
|
|
2596
|
+
const config = registry.get('manager-name');
|
|
2597
|
+
|
|
2598
|
+
// Wait for a manager to be defined and all setups to complete
|
|
2599
|
+
await registry.whenDefined('manager-name');
|
|
2600
|
+
```
|
|
2601
|
+
|
|
2602
|
+
**Methods:**
|
|
2603
|
+
|
|
2604
|
+
- `define(name, config)` - Register a manager configuration
|
|
2605
|
+
- Throws `Error: Already registered` if name already exists
|
|
2606
|
+
- Dispatches an event with the manager name when successful
|
|
2607
|
+
|
|
2608
|
+
- `get(name)` - Retrieve a manager configuration
|
|
2609
|
+
- Returns the configuration or `undefined` if not found
|
|
2610
|
+
|
|
2611
|
+
- `whenDefined(name)` - Wait for manager definition and setup completion
|
|
2612
|
+
- Returns a Promise that resolves when:
|
|
2613
|
+
1. The manager is defined (waits for definition if not yet registered)
|
|
2614
|
+
2. All pending `ish` property setups for this manager are complete
|
|
2615
|
+
- This is the recommended way to wait for async manager instantiation
|
|
2616
|
+
|
|
2617
|
+
### Manager Configuration
|
|
2618
|
+
|
|
2619
|
+
Manager configurations follow this interface:
|
|
2620
|
+
|
|
2621
|
+
```TypeScript
|
|
2622
|
+
interface ItemscopeManagerConfig<T = any> {
|
|
2623
|
+
manager: {
|
|
2624
|
+
new (element: HTMLElement, initVals?: Partial<T>): T;
|
|
2625
|
+
};
|
|
2626
|
+
lifecycleKeys?: {
|
|
2627
|
+
dispose?: string | symbol;
|
|
2628
|
+
resolved?: string | symbol;
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
```
|
|
2632
|
+
|
|
2633
|
+
**Properties:**
|
|
2634
|
+
|
|
2635
|
+
- `manager` (required): Constructor function that receives:
|
|
2636
|
+
- `element`: The HTMLElement with the itemscope attribute
|
|
2637
|
+
- `initVals`: Merged values from all queued `ish` assignments
|
|
2638
|
+
|
|
2639
|
+
- `lifecycleKeys` (optional): Lifecycle method names
|
|
2640
|
+
- `dispose`: Method to call when cleaning up
|
|
2641
|
+
- `resolved`: Property/event name for async initialization
|
|
2642
|
+
|
|
2643
|
+
### Lazy Registration
|
|
2644
|
+
|
|
2645
|
+
Managers can be registered after elements are already using them. The system queues values and instantiates the manager when it's registered:
|
|
2646
|
+
|
|
2647
|
+
```TypeScript
|
|
2648
|
+
const element = document.createElement('div');
|
|
2649
|
+
element.setAttribute('itemscope', 'lazy-manager');
|
|
2650
|
+
|
|
2651
|
+
// Assign before manager is registered - values are queued
|
|
2652
|
+
element.assignGingerly({ ish: { prop1: 'value1' } });
|
|
2653
|
+
element.assignGingerly({ ish: { prop2: 'value2' } });
|
|
2654
|
+
|
|
2655
|
+
// Register the manager later
|
|
2656
|
+
setTimeout(() => {
|
|
2657
|
+
customElements.itemscopeRegistry.define('lazy-manager', {
|
|
2658
|
+
manager: class LazyManager {
|
|
2659
|
+
constructor(element, initVals) {
|
|
2660
|
+
this.element = element;
|
|
2661
|
+
Object.assign(this, initVals);
|
|
2662
|
+
// initVals contains both prop1 and prop2
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}, 100);
|
|
2667
|
+
|
|
2668
|
+
// Wait for registration and setup
|
|
2669
|
+
await customElements.itemscopeRegistry.whenDefined('lazy-manager');
|
|
2670
|
+
|
|
2671
|
+
console.log(element.ish.prop1); // 'value1'
|
|
2672
|
+
console.log(element.ish.prop2); // 'value2'
|
|
2673
|
+
```
|
|
2674
|
+
|
|
2675
|
+
### Instance Caching
|
|
2676
|
+
|
|
2677
|
+
Manager instances are cached per element. Subsequent `ish` assignments merge values into the existing instance:
|
|
2678
|
+
|
|
2679
|
+
```TypeScript
|
|
2680
|
+
const element = document.createElement('div');
|
|
2681
|
+
element.setAttribute('itemscope', 'my-manager');
|
|
2682
|
+
|
|
2683
|
+
// First assignment - creates instance
|
|
2684
|
+
element.assignGingerly({ ish: { prop1: 'value1' } });
|
|
2685
|
+
await customElements.itemscopeRegistry.whenDefined('my-manager');
|
|
2686
|
+
|
|
2687
|
+
const firstInstance = element.ish;
|
|
2688
|
+
|
|
2689
|
+
// Second assignment - reuses instance
|
|
2690
|
+
element.assignGingerly({ ish: { prop2: 'value2' } });
|
|
2691
|
+
await customElements.itemscopeRegistry.whenDefined('my-manager');
|
|
2692
|
+
|
|
2693
|
+
console.log(element.ish === firstInstance); // true - same instance
|
|
2694
|
+
console.log(element.ish.prop1); // 'value1'
|
|
2695
|
+
console.log(element.ish.prop2); // 'value2'
|
|
2696
|
+
```
|
|
2697
|
+
|
|
2698
|
+
### Validation and Error Handling
|
|
2699
|
+
|
|
2700
|
+
The system validates `ish` property assignments and throws descriptive errors:
|
|
2701
|
+
|
|
2702
|
+
```TypeScript
|
|
2703
|
+
// Error: Element must have itemscope attribute
|
|
2704
|
+
const div1 = document.createElement('div');
|
|
2705
|
+
div1.assignGingerly({ ish: { prop: 'value' } });
|
|
2706
|
+
// Throws asynchronously
|
|
2707
|
+
|
|
2708
|
+
// Error: itemscope must be non-empty string
|
|
2709
|
+
const div2 = document.createElement('div');
|
|
2710
|
+
div2.setAttribute('itemscope', '');
|
|
2711
|
+
div2.assignGingerly({ ish: { prop: 'value' } });
|
|
2712
|
+
// Throws asynchronously
|
|
2713
|
+
|
|
2714
|
+
// Error: ish value must be an object
|
|
2715
|
+
const div3 = document.createElement('div');
|
|
2716
|
+
div3.setAttribute('itemscope', 'my-manager');
|
|
2717
|
+
div3.assignGingerly({ ish: 'string' });
|
|
2718
|
+
// Throws asynchronously
|
|
2719
|
+
```
|
|
2720
|
+
|
|
2721
|
+
**Note**: Errors are thrown asynchronously since the `ish` property setup happens in the background. They will appear in the console but won't be catchable with try/catch around the `assignGingerly` call.
|
|
2722
|
+
|
|
2723
|
+
### Scoped Registries
|
|
2724
|
+
|
|
2725
|
+
ItemScope Managers integrate with scoped custom element registries. Each element can have its own registry:
|
|
2726
|
+
|
|
2727
|
+
```TypeScript
|
|
2728
|
+
// Create a scoped registry
|
|
2729
|
+
const scopedRegistry = new CustomElementRegistry();
|
|
2730
|
+
|
|
2731
|
+
// Define a manager in the scoped registry
|
|
2732
|
+
scopedRegistry.itemscopeRegistry.define('scoped-manager', {
|
|
2733
|
+
manager: ScopedManager
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
// Attach the registry to an element
|
|
2737
|
+
const element = document.createElement('div');
|
|
2738
|
+
element.customElementRegistry = scopedRegistry;
|
|
2739
|
+
element.setAttribute('itemscope', 'scoped-manager');
|
|
2740
|
+
|
|
2741
|
+
// The element uses its scoped registry
|
|
2742
|
+
element.assignGingerly({ ish: { prop: 'value' } });
|
|
2743
|
+
await scopedRegistry.itemscopeRegistry.whenDefined('scoped-manager');
|
|
2744
|
+
```
|
|
2745
|
+
|
|
2746
|
+
If an element doesn't have a `customElementRegistry` property, it falls back to the global `customElements.itemscopeRegistry`.
|
|
2747
|
+
|
|
2748
|
+
### Complete Example
|
|
2749
|
+
|
|
2750
|
+
```html
|
|
2751
|
+
<!DOCTYPE html>
|
|
2752
|
+
<html>
|
|
2753
|
+
<head>
|
|
2754
|
+
<script type="module">
|
|
2755
|
+
import 'assign-gingerly/object-extension.js';
|
|
2756
|
+
|
|
2757
|
+
// Define a todo item manager
|
|
2758
|
+
class TodoItemManager {
|
|
2759
|
+
element;
|
|
2760
|
+
text = '';
|
|
2761
|
+
completed = false;
|
|
2762
|
+
|
|
2763
|
+
constructor(element, initVals) {
|
|
2764
|
+
this.element = element;
|
|
2765
|
+
if (initVals) {
|
|
2766
|
+
Object.assign(this, initVals);
|
|
2767
|
+
}
|
|
2768
|
+
this.render();
|
|
2769
|
+
this.attachListeners();
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
render() {
|
|
2773
|
+
const checkbox = this.element.querySelector('input[type="checkbox"]');
|
|
2774
|
+
const label = this.element.querySelector('label');
|
|
2775
|
+
|
|
2776
|
+
if (checkbox) checkbox.checked = this.completed;
|
|
2777
|
+
if (label) label.textContent = this.text;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
attachListeners() {
|
|
2781
|
+
const checkbox = this.element.querySelector('input[type="checkbox"]');
|
|
2782
|
+
if (checkbox) {
|
|
2783
|
+
checkbox.addEventListener('change', (e) => {
|
|
2784
|
+
this.completed = e.target.checked;
|
|
2785
|
+
this.render();
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
cleanup() {
|
|
2791
|
+
// Remove event listeners, etc.
|
|
2792
|
+
console.log('Cleaning up todo item');
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// Register the manager
|
|
2797
|
+
customElements.itemscopeRegistry.define('todo-item', {
|
|
2798
|
+
manager: TodoItemManager,
|
|
2799
|
+
lifecycleKeys: {
|
|
2800
|
+
dispose: 'cleanup'
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
|
|
2804
|
+
// Initialize todo items
|
|
2805
|
+
async function initTodos() {
|
|
2806
|
+
const items = document.querySelectorAll('[itemscope="todo-item"]');
|
|
2807
|
+
|
|
2808
|
+
items.forEach((item, index) => {
|
|
2809
|
+
item.assignGingerly({
|
|
2810
|
+
ish: {
|
|
2811
|
+
text: `Todo item ${index + 1}`,
|
|
2812
|
+
completed: false
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
// Wait for all setups to complete
|
|
2818
|
+
await customElements.itemscopeRegistry.whenDefined('todo-item');
|
|
2819
|
+
|
|
2820
|
+
console.log('All todo items initialized');
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// Run on page load
|
|
2824
|
+
document.addEventListener('DOMContentLoaded', initTodos);
|
|
2825
|
+
</script>
|
|
2826
|
+
</head>
|
|
2827
|
+
<body>
|
|
2828
|
+
<h1>Todo List</h1>
|
|
2829
|
+
<ul>
|
|
2830
|
+
<li itemscope="todo-item">
|
|
2831
|
+
<input type="checkbox">
|
|
2832
|
+
<label></label>
|
|
2833
|
+
</li>
|
|
2834
|
+
<li itemscope="todo-item">
|
|
2835
|
+
<input type="checkbox">
|
|
2836
|
+
<label></label>
|
|
2837
|
+
</li>
|
|
2838
|
+
<li itemscope="todo-item">
|
|
2839
|
+
<input type="checkbox">
|
|
2840
|
+
<label></label>
|
|
2841
|
+
</li>
|
|
2842
|
+
</ul>
|
|
2843
|
+
</body>
|
|
2844
|
+
</html>
|
|
2845
|
+
```
|
|
2846
|
+
|
|
2847
|
+
### Testing with whenDefined
|
|
2848
|
+
|
|
2849
|
+
When writing tests for code that uses ItemScope Managers, use `whenDefined()` to wait for async setup:
|
|
2850
|
+
|
|
2851
|
+
```TypeScript
|
|
2852
|
+
// Test example
|
|
2853
|
+
test('should initialize manager with values', async () => {
|
|
2854
|
+
const element = document.createElement('div');
|
|
2855
|
+
element.setAttribute('itemscope', 'test-manager');
|
|
2856
|
+
|
|
2857
|
+
// Register manager
|
|
2858
|
+
customElements.itemscopeRegistry.define('test-manager', {
|
|
2859
|
+
manager: class TestManager {
|
|
2860
|
+
constructor(element, initVals) {
|
|
2861
|
+
this.element = element;
|
|
2862
|
+
Object.assign(this, initVals);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
// Assign values
|
|
2868
|
+
element.assignGingerly({ ish: { prop: 'value' } });
|
|
2869
|
+
|
|
2870
|
+
// Wait for setup to complete
|
|
2871
|
+
await customElements.itemscopeRegistry.whenDefined('test-manager');
|
|
2872
|
+
|
|
2873
|
+
// Now we can assert
|
|
2874
|
+
expect(element.ish.prop).toBe('value');
|
|
2875
|
+
expect(element.ish.element).toBe(element);
|
|
2876
|
+
});
|
|
2877
|
+
```
|
|
2878
|
+
|
|
2879
|
+
### Design Rationale
|
|
2880
|
+
|
|
2881
|
+
ItemScope Managers follow these design principles:
|
|
2882
|
+
|
|
2883
|
+
1. **Synchronous API**: `assignGingerly` remains synchronous and returns immediately
|
|
2884
|
+
2. **Async setup**: Manager instantiation happens asynchronously in the background
|
|
2885
|
+
3. **Explicit waiting**: Use `whenDefined()` when you need to wait for setup completion
|
|
2886
|
+
4. **Dual behavior**: The `ish` property has special meaning only for HTMLElements with `itemscope` attributes
|
|
2887
|
+
5. **Registry-based**: Follows the same pattern as `EnhancementRegistry` for consistency
|
|
2888
|
+
6. **Event-driven**: Uses EventTarget for lazy registration support
|
|
2889
|
+
|
|
2890
|
+
This design ensures backward compatibility while providing powerful new capabilities for managing DOM fragments.
|
|
2891
|
+
|
|
2455
2892
|
|
|
2456
2893
|
|
|
2457
2894
|
|