assign-gingerly 0.0.22 → 0.0.24
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 +713 -40
- package/assignGingerly.js +193 -9
- package/assignGingerly.ts +237 -9
- 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 +3 -3
- package/parseWithAttrs.js +29 -23
- package/parseWithAttrs.ts +41 -24
- package/playwright.config.ts +9 -9
- package/types/assign-gingerly/types.d.ts +80 -23
- package/types/global.d.ts +4 -0
- package/waitForEvent.js +15 -1
- package/waitForEvent.ts +19 -1
package/README.md
CHANGED
|
@@ -23,16 +23,24 @@ 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. Default Support For Not Replacing one object with another if it is a subclass. [TODO]
|
|
37
|
+
|
|
38
|
+
4. Custom Element Features [TODO]
|
|
33
39
|
|
|
34
40
|
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
41
|
|
|
42
|
+
5. Iterator upgrade support [TODO] -- limited to ish?
|
|
43
|
+
|
|
36
44
|
Anyway, let's start out detailing the more innocent features of this package / polyfill.
|
|
37
45
|
|
|
38
46
|
The two utility functions are:
|
|
@@ -102,7 +110,220 @@ console.log(obj);
|
|
|
102
110
|
|
|
103
111
|
When the right hand side of an expression is an object, assignGingerly is recursively applied (passing the third argument in if applicable, which will be discussed below).
|
|
104
112
|
|
|
105
|
-
Of course, just as Object.assign led to object spread notation, assignGingerly could lead to some sort of deep structural JavaScript syntax, but that is outside the scope of this polyfill package.
|
|
113
|
+
Of course, just as Object.assign led to object spread notation, assignGingerly could lead to some sort of deep structural JavaScript syntax, but that is outside the scope of this polyfill package.
|
|
114
|
+
|
|
115
|
+
## Example 3a - Automatic Readonly Property Detection
|
|
116
|
+
|
|
117
|
+
assignGingerly automatically detects readonly properties and merges into them instead of attempting to replace them. This makes working with DOM properties like `style` and `dataset` much more ergonomic:
|
|
118
|
+
|
|
119
|
+
```TypeScript
|
|
120
|
+
// Instead of this verbose syntax:
|
|
121
|
+
const div = document.createElement('div');
|
|
122
|
+
assignGingerly(div, {
|
|
123
|
+
'?.style?.height': '15px',
|
|
124
|
+
'?.style?.width': '20px'
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// You can now use this cleaner syntax:
|
|
128
|
+
assignGingerly(div, {
|
|
129
|
+
style: {
|
|
130
|
+
height: '15px',
|
|
131
|
+
width: '20px'
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
console.log(div.style.height); // '15px'
|
|
135
|
+
console.log(div.style.width); // '20px'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**How it works:**
|
|
139
|
+
|
|
140
|
+
When assignGingerly encounters an object value being assigned to an existing property, it checks if that property is readonly:
|
|
141
|
+
- **Data properties** with `writable: false`
|
|
142
|
+
- **Accessor properties** with a getter but no setter
|
|
143
|
+
|
|
144
|
+
If the property is readonly and its current value is an object, assignGingerly automatically merges into it recursively.
|
|
145
|
+
|
|
146
|
+
**Examples of readonly properties:**
|
|
147
|
+
- `HTMLElement.style` - The CSSStyleDeclaration object
|
|
148
|
+
- `HTMLElement.dataset` - The DOMStringMap object
|
|
149
|
+
- Custom objects with `Object.defineProperty(obj, 'prop', { value: {}, writable: false })`
|
|
150
|
+
- Accessor properties with getter only: `Object.defineProperty(obj, 'prop', { get() { return {}; } })`
|
|
151
|
+
|
|
152
|
+
**Error handling:**
|
|
153
|
+
|
|
154
|
+
If you try to merge an object into a readonly property whose current value is a primitive, assignGingerly throws a descriptive error:
|
|
155
|
+
|
|
156
|
+
```TypeScript
|
|
157
|
+
const obj = {};
|
|
158
|
+
Object.defineProperty(obj, 'readonlyString', {
|
|
159
|
+
value: 'immutable',
|
|
160
|
+
writable: false
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assignGingerly(obj, {
|
|
164
|
+
readonlyString: { nested: 'value' }
|
|
165
|
+
});
|
|
166
|
+
// Error: Cannot merge object into readonly primitive property 'readonlyString'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Additional examples:**
|
|
170
|
+
|
|
171
|
+
```TypeScript
|
|
172
|
+
// Dataset property
|
|
173
|
+
const div = document.createElement('div');
|
|
174
|
+
assignGingerly(div, {
|
|
175
|
+
dataset: {
|
|
176
|
+
userId: '123',
|
|
177
|
+
userName: 'Alice'
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
console.log(div.dataset.userId); // '123'
|
|
181
|
+
console.log(div.dataset.userName); // 'Alice'
|
|
182
|
+
|
|
183
|
+
// Custom readonly property
|
|
184
|
+
const config = {};
|
|
185
|
+
Object.defineProperty(config, 'settings', {
|
|
186
|
+
value: {},
|
|
187
|
+
writable: false
|
|
188
|
+
});
|
|
189
|
+
assignGingerly(config, {
|
|
190
|
+
settings: {
|
|
191
|
+
theme: 'dark',
|
|
192
|
+
lang: 'en'
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
console.log(config.settings.theme); // 'dark'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Example 3b - Automatic Class Instance Preservation
|
|
199
|
+
|
|
200
|
+
In addition to readonly property detection, assignGingerly automatically preserves class instances when merging. This is particularly useful when working with enhancement instances:
|
|
201
|
+
|
|
202
|
+
```TypeScript
|
|
203
|
+
import 'assign-gingerly/object-extension.js';
|
|
204
|
+
|
|
205
|
+
// Define an enhancement class
|
|
206
|
+
class MyEnhancement {
|
|
207
|
+
constructor(element, ctx, initVals) {
|
|
208
|
+
this.element = element;
|
|
209
|
+
this.instanceId = Math.random(); // Track instance identity
|
|
210
|
+
if (initVals) {
|
|
211
|
+
Object.assign(this, initVals);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
prop1 = null;
|
|
215
|
+
prop2 = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const element = document.createElement('div');
|
|
219
|
+
element.enh = {
|
|
220
|
+
myEnh: new MyEnhancement(element, {}, {})
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const originalId = element.enh.myEnh.instanceId;
|
|
224
|
+
|
|
225
|
+
// Clean syntax - no need for ?.myEnh?.prop1 notation
|
|
226
|
+
assignGingerly(element, {
|
|
227
|
+
enh: {
|
|
228
|
+
myEnh: {
|
|
229
|
+
prop1: 'value1',
|
|
230
|
+
prop2: 'value2'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
console.log(element.enh.myEnh.instanceId === originalId); // true - instance preserved!
|
|
236
|
+
console.log(element.enh.myEnh.prop1); // 'value1'
|
|
237
|
+
console.log(element.enh.myEnh.prop2); // 'value2'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**How it works:**
|
|
241
|
+
|
|
242
|
+
When assignGingerly encounters an object value being assigned to an existing property, it checks if the current value is a class instance (not a plain object):
|
|
243
|
+
|
|
244
|
+
- **Class instances** are detected by checking if their prototype is something other than `Object.prototype` or `null`
|
|
245
|
+
- **Plain objects** `{}` have `Object.prototype` as their prototype
|
|
246
|
+
- **Class instances** have their class's prototype
|
|
247
|
+
|
|
248
|
+
If the existing value is a class instance, assignGingerly merges into it instead of replacing it.
|
|
249
|
+
|
|
250
|
+
**What counts as a class instance:**
|
|
251
|
+
- Custom class instances: `new MyClass()`
|
|
252
|
+
- Built-in class instances: `new Date()`, `new Map()`, `new Set()`, etc.
|
|
253
|
+
- Enhancement instances on the `enh` property
|
|
254
|
+
- Any object whose prototype is not `Object.prototype` or `null`
|
|
255
|
+
|
|
256
|
+
**What doesn't count:**
|
|
257
|
+
- Plain objects: `{}`, `{ a: 1 }`
|
|
258
|
+
- Arrays: `[]`, `[1, 2, 3]` (arrays are replaced, not merged)
|
|
259
|
+
- Primitives: strings, numbers, booleans
|
|
260
|
+
|
|
261
|
+
**Benefits:**
|
|
262
|
+
|
|
263
|
+
This feature enables clean, framework-friendly syntax for updating enhancements:
|
|
264
|
+
|
|
265
|
+
```TypeScript
|
|
266
|
+
// Before: Verbose nested path syntax
|
|
267
|
+
assignGingerly(element, {
|
|
268
|
+
'?.enh?.mellowYellow?.madAboutFourteen': true
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// After: Clean object syntax
|
|
272
|
+
assignGingerly(element, {
|
|
273
|
+
enh: {
|
|
274
|
+
mellowYellow: {
|
|
275
|
+
madAboutFourteen: true
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Additional examples:**
|
|
282
|
+
|
|
283
|
+
```TypeScript
|
|
284
|
+
// Multiple enhancements at once
|
|
285
|
+
assignGingerly(element, {
|
|
286
|
+
enh: {
|
|
287
|
+
enhancement1: { prop: 'value1' },
|
|
288
|
+
enhancement2: { prop: 'value2' }
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Works with built-in classes too
|
|
293
|
+
const obj = {
|
|
294
|
+
timestamp: new Date('2024-01-01')
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
assignGingerly(obj, {
|
|
298
|
+
timestamp: {
|
|
299
|
+
customProp: 'metadata'
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
console.log(obj.timestamp instanceof Date); // true - Date instance preserved
|
|
304
|
+
console.log(obj.timestamp.customProp); // 'metadata'
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Combined with readonly detection:**
|
|
308
|
+
|
|
309
|
+
Both readonly properties and class instances are preserved:
|
|
310
|
+
|
|
311
|
+
```TypeScript
|
|
312
|
+
const div = document.createElement('div');
|
|
313
|
+
div.enh = {
|
|
314
|
+
myEnh: new MyEnhancement(div, {}, {})
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
assignGingerly(div, {
|
|
318
|
+
style: { height: '100px' }, // Readonly - merged
|
|
319
|
+
enh: {
|
|
320
|
+
myEnh: { prop: 'value' } // Class instance - merged
|
|
321
|
+
},
|
|
322
|
+
dataset: { userId: '123' } // Readonly - merged
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// All instances and readonly objects preserved
|
|
326
|
+
```
|
|
106
327
|
|
|
107
328
|
While we are in the business of passing values of object A into object B, we might as well add some extremely common behavior that allows updating properties of object B based on the current values of object B -- things like incrementing, toggling, and deleting. Deleting is critical for assignTentatively, but is included with both functions.
|
|
108
329
|
|
|
@@ -167,7 +388,7 @@ For existing values, the toggle is performed using JavaScript's logical NOT oper
|
|
|
167
388
|
|
|
168
389
|
## Example 6 - Deleting properties with -= command
|
|
169
390
|
|
|
170
|
-
The `-=` command allows
|
|
391
|
+
The `-=` command allows us to delete properties from objects:
|
|
171
392
|
|
|
172
393
|
```TypeScript
|
|
173
394
|
const obj = {
|
|
@@ -296,7 +517,7 @@ interface IEnhancementRegistryItem<T = any, TObjToExtend = any> {
|
|
|
296
517
|
spawn: {new(objToExtend: TObjToExtend, ctx: SpawnContext, initVals: Partial<T>): T}
|
|
297
518
|
symlinks?: {[key: symbol]: keyof T}
|
|
298
519
|
// Optional: for element enhancement access
|
|
299
|
-
enhKey?: string
|
|
520
|
+
enhKey?: string | symbol
|
|
300
521
|
// Optional: automatic attribute parsing
|
|
301
522
|
withAttrs?: AttrPatterns<T>
|
|
302
523
|
}
|
|
@@ -332,7 +553,7 @@ EnhancementRegistry.push([
|
|
|
332
553
|
},
|
|
333
554
|
spawn: MyEnhancement,
|
|
334
555
|
},{
|
|
335
|
-
|
|
556
|
+
enhKey: 'mellowYellow',
|
|
336
557
|
symlinks: {
|
|
337
558
|
[isMellow]: 'isMellow'
|
|
338
559
|
},
|
|
@@ -344,8 +565,13 @@ EnhancementRegistry.push([
|
|
|
344
565
|
const result = assignGingerly({}, {
|
|
345
566
|
[isHappy]: true,
|
|
346
567
|
[isMellow]: true,
|
|
347
|
-
|
|
348
|
-
|
|
568
|
+
style:{
|
|
569
|
+
height: '40px',
|
|
570
|
+
},
|
|
571
|
+
enh: {
|
|
572
|
+
'?.mellowYellow?.madAboutFourteen': true
|
|
573
|
+
}
|
|
574
|
+
|
|
349
575
|
}, {
|
|
350
576
|
registry: EnhancementRegistry
|
|
351
577
|
});
|
|
@@ -462,8 +688,12 @@ console.log(target.set[symbol1].prop2); // 'value2'
|
|
|
462
688
|
const result = assignGingerly({}, {
|
|
463
689
|
"[Symbol.for('TFWsx0YH5E6eSfhE7zfLxA')]": true,
|
|
464
690
|
"[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
|
|
465
|
-
|
|
466
|
-
|
|
691
|
+
style: {
|
|
692
|
+
height: '40px'
|
|
693
|
+
}
|
|
694
|
+
enh: {
|
|
695
|
+
mellowYellow?.madAboutFourteen': true
|
|
696
|
+
}
|
|
467
697
|
}, {
|
|
468
698
|
registry: EnhancementRegistry
|
|
469
699
|
});
|
|
@@ -511,12 +741,16 @@ The prototype extensions are non-enumerable and won't appear in `Object.keys()`
|
|
|
511
741
|
|
|
512
742
|
-->
|
|
513
743
|
|
|
514
|
-
## Custom Element Registry
|
|
744
|
+
## Enhancement Registry Addendum to the Custom Element Registry
|
|
745
|
+
|
|
746
|
+
This package polyfill adds an "enhancementRegistry" registry on the CustomElementRegistry prototype.
|
|
515
747
|
|
|
516
|
-
|
|
748
|
+
In this way, we achieve dependency injection in harmony with scoped custom elements DOM registry scopes.
|
|
517
749
|
|
|
518
750
|
> [!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.
|
|
751
|
+
> 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.
|
|
752
|
+
>
|
|
753
|
+
> For more information about scoped custom element registries, see [Chrome's announcement and guide](https://developer.chrome.com/blog/scoped-registries).
|
|
520
754
|
|
|
521
755
|
<details>
|
|
522
756
|
<summary>Automatic Registry Population</summary>
|
|
@@ -589,7 +823,7 @@ Building on the Custom Element Registry integration, this package provides a pow
|
|
|
589
823
|
|
|
590
824
|
### Basic Usage
|
|
591
825
|
|
|
592
|
-
The `enh.set` proxy allows
|
|
826
|
+
The `enh.set` proxy allows us to assign properties to enhancements using a clean, chainable syntax:
|
|
593
827
|
|
|
594
828
|
```TypeScript
|
|
595
829
|
import 'assign-gingerly/object-extension.js';
|
|
@@ -680,12 +914,12 @@ class Enhancement<T> {
|
|
|
680
914
|
|
|
681
915
|
All parameters are optional for backward compatibility with existing code.
|
|
682
916
|
|
|
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
|
|
917
|
+
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
918
|
|
|
685
919
|
<details>
|
|
686
920
|
<summary>Passing Custom Context</summary>
|
|
687
921
|
|
|
688
|
-
You can pass custom context when calling `enh.get()` or `enh.whenResolved()
|
|
922
|
+
You can pass custom context when calling `enh.get()` or `enh.whenResolved()` (discussed in detail below):
|
|
689
923
|
|
|
690
924
|
```TypeScript
|
|
691
925
|
// Pass custom context to the spawned instance
|
|
@@ -1411,7 +1645,8 @@ console.log(instance.count); // 42 (parsed from attribute)
|
|
|
1411
1645
|
console.log(instance.theme); // 'dark' (parsed from attribute)
|
|
1412
1646
|
```
|
|
1413
1647
|
|
|
1414
|
-
|
|
1648
|
+
<details>
|
|
1649
|
+
<summary>Example without enhKey</summary>
|
|
1415
1650
|
|
|
1416
1651
|
```TypeScript
|
|
1417
1652
|
// withAttrs works even without enhKey
|
|
@@ -1445,16 +1680,21 @@ const instance = element.enh.get(config);
|
|
|
1445
1680
|
console.log(instance.value); // 'test123' (parsed from attribute)
|
|
1446
1681
|
```
|
|
1447
1682
|
|
|
1448
|
-
|
|
1683
|
+
</details>
|
|
1684
|
+
|
|
1685
|
+
<details>
|
|
1686
|
+
<summary>How it works</summary>
|
|
1687
|
+
|
|
1449
1688
|
1. When an enhancement is spawned via `enh.get()`, `enh.set`, or `assignGingerly()`
|
|
1450
1689
|
2. If the registry item has a `withAttrs` property defined
|
|
1451
1690
|
3. `parseWithAttrs(element, registryItem.withAttrs)` is automatically called
|
|
1452
1691
|
4. The parsed attributes are passed to the enhancement constructor as `initVals`
|
|
1453
1692
|
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)
|
|
1454
1693
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1694
|
+
</details>
|
|
1457
1695
|
|
|
1696
|
+
> ![NOTE]
|
|
1697
|
+
> `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.
|
|
1458
1698
|
|
|
1459
1699
|
### The `enh-` Prefix for Attribute Isolation
|
|
1460
1700
|
|
|
@@ -1652,7 +1892,10 @@ interface AttrPatterns<T> {
|
|
|
1652
1892
|
interface AttrConfig<T> {
|
|
1653
1893
|
mapsTo?: keyof T | '.'; // Target property name (or '.' to spread)
|
|
1654
1894
|
instanceOf?: string | Function; // Type for default parser
|
|
1655
|
-
parser?:
|
|
1895
|
+
parser?:
|
|
1896
|
+
| ((v: string | null) => any) // Inline parser function
|
|
1897
|
+
| string // Named parser from globalParserRegistry
|
|
1898
|
+
| [string, string]; // [CustomElementName, StaticMethodName]
|
|
1656
1899
|
}
|
|
1657
1900
|
```
|
|
1658
1901
|
|
|
@@ -1772,7 +2015,7 @@ The following parsers are pre-registered in `globalParserRegistry`:
|
|
|
1772
2015
|
|
|
1773
2016
|
**Custom Element Static Method Parsers:**
|
|
1774
2017
|
|
|
1775
|
-
You can
|
|
2018
|
+
You can reference static methods on custom elements using tuple syntax `[elementName, methodName]`:
|
|
1776
2019
|
|
|
1777
2020
|
```TypeScript
|
|
1778
2021
|
class MyWidget extends HTMLElement {
|
|
@@ -1786,29 +2029,47 @@ class MyWidget extends HTMLElement {
|
|
|
1786
2029
|
}
|
|
1787
2030
|
customElements.define('my-widget', MyWidget);
|
|
1788
2031
|
|
|
1789
|
-
// Reference custom element parsers
|
|
2032
|
+
// Reference custom element parsers using tuple syntax
|
|
1790
2033
|
const config = {
|
|
1791
2034
|
base: 'data-',
|
|
1792
2035
|
value: '${base}value',
|
|
1793
2036
|
_value: {
|
|
1794
|
-
parser: 'my-widget
|
|
2037
|
+
parser: ['my-widget', 'parseSpecialFormat'] // [element-name, methodName]
|
|
2038
|
+
},
|
|
2039
|
+
title: '${base}title',
|
|
2040
|
+
_title: {
|
|
2041
|
+
parser: ['my-widget', 'parseWithPrefix']
|
|
1795
2042
|
}
|
|
1796
2043
|
};
|
|
2044
|
+
|
|
2045
|
+
const result = parseWithAttrs(element, config);
|
|
1797
2046
|
```
|
|
1798
2047
|
|
|
1799
|
-
**Parser Resolution
|
|
2048
|
+
**Parser Resolution:**
|
|
1800
2049
|
|
|
1801
|
-
When a
|
|
2050
|
+
When a parser is specified, it can be:
|
|
1802
2051
|
|
|
1803
|
-
1. **
|
|
1804
|
-
2. **
|
|
1805
|
-
3. **
|
|
1806
|
-
|
|
2052
|
+
1. **Inline function** - `parser: (v) => v.toUpperCase()` - Used directly
|
|
2053
|
+
2. **String reference** - `parser: 'timestamp'` - Looks up in `globalParserRegistry`
|
|
2054
|
+
3. **Tuple reference** - `parser: ['my-widget', 'parseMethod']` - Looks up static method on custom element constructor
|
|
2055
|
+
|
|
2056
|
+
**Error Handling:**
|
|
2057
|
+
|
|
2058
|
+
The tuple syntax provides clear error messages:
|
|
2059
|
+
|
|
2060
|
+
```TypeScript
|
|
2061
|
+
// Element not found
|
|
2062
|
+
parser: ['non-existent', 'method']
|
|
2063
|
+
// Error: Cannot resolve parser [non-existent, method]: custom element "non-existent" not found
|
|
1807
2064
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
-
|
|
1811
|
-
|
|
2065
|
+
// Method not found
|
|
2066
|
+
parser: ['my-widget', 'nonExistent']
|
|
2067
|
+
// Error: Cannot resolve parser [my-widget, nonExistent]: static method "nonExistent" not found on custom element "my-widget"
|
|
2068
|
+
|
|
2069
|
+
// String not found in registry
|
|
2070
|
+
parser: 'unknown'
|
|
2071
|
+
// Error: Parser "unknown" not found in globalParserRegistry. If you want to reference a custom element static method, use tuple syntax: ["element-name", "methodName"]
|
|
2072
|
+
```
|
|
1812
2073
|
|
|
1813
2074
|
**Example: Organizing Parsers**
|
|
1814
2075
|
|
|
@@ -1890,7 +2151,7 @@ const result = parseWithAttrs(element, {
|
|
|
1890
2151
|
|
|
1891
2152
|
### Default Values with valIfNull
|
|
1892
2153
|
|
|
1893
|
-
The `valIfNull` property allows
|
|
2154
|
+
The `valIfNull` property allows us to specify default values when attributes are missing:
|
|
1894
2155
|
|
|
1895
2156
|
```TypeScript
|
|
1896
2157
|
// HTML: <div></div> (no attributes)
|
|
@@ -2357,13 +2618,17 @@ buildCSSQuery(config, ' div , span , p ');
|
|
|
2357
2618
|
1. **Mount Observer Integration**: Find elements that need enhancement
|
|
2358
2619
|
```TypeScript
|
|
2359
2620
|
// Match any element with the attributes
|
|
2360
|
-
const
|
|
2361
|
-
const observer = new
|
|
2362
|
-
|
|
2363
|
-
|
|
2621
|
+
const matching = buildCSSQuery(enhancementConfig);
|
|
2622
|
+
const observer = new MountObserver({
|
|
2623
|
+
matching,
|
|
2624
|
+
do: (mountedElement) => {
|
|
2625
|
+
enhance(mountedElement);
|
|
2626
|
+
}
|
|
2364
2627
|
});
|
|
2365
2628
|
```
|
|
2366
2629
|
|
|
2630
|
+
See [Mount-Observer](https://github.com/bahrus/mount-observer).
|
|
2631
|
+
|
|
2367
2632
|
2. **Specific Element Types**: Enhance only certain element types
|
|
2368
2633
|
```TypeScript
|
|
2369
2634
|
const query = buildCSSQuery(config, 'template, script');
|
|
@@ -2452,6 +2717,414 @@ console.log(result);
|
|
|
2452
2717
|
|
|
2453
2718
|
-->
|
|
2454
2719
|
|
|
2720
|
+
## Itemscope Managers (Chrome 146+)
|
|
2721
|
+
|
|
2722
|
+
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.
|
|
2723
|
+
|
|
2724
|
+
> [!NOTE]
|
|
2725
|
+
> This feature requires Chrome 146+ with scoped custom element registry support. It follows the same browser support requirements as the Enhancement Registry integration.
|
|
2726
|
+
>
|
|
2727
|
+
> For more information about scoped custom element registries, see [Chrome's announcement and guide](https://developer.chrome.com/blog/scoped-registries).
|
|
2728
|
+
|
|
2729
|
+
### Why Itemscope Managers?
|
|
2730
|
+
|
|
2731
|
+
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:
|
|
2732
|
+
|
|
2733
|
+
- **Manage light children**: Attach behavior to light DOM children of web components without wrapping them in custom elements
|
|
2734
|
+
- **Handle template loops**: Manage repeated DOM fragments generated by template systems
|
|
2735
|
+
- **Avoid custom element overhead**: Enhance elements where custom element registration isn't appropriate or possible
|
|
2736
|
+
- **Separate concerns**: Keep data/view model logic separate from the DOM structure
|
|
2737
|
+
|
|
2738
|
+
### Basic Usage
|
|
2739
|
+
|
|
2740
|
+
```html
|
|
2741
|
+
<div itemscope="user-card">
|
|
2742
|
+
<h2>User Profile</h2>
|
|
2743
|
+
<p itemprop="name"></p>
|
|
2744
|
+
<p itemprop="email"></p>
|
|
2745
|
+
</div>
|
|
2746
|
+
```
|
|
2747
|
+
|
|
2748
|
+
```TypeScript
|
|
2749
|
+
import 'assign-gingerly/object-extension.js';
|
|
2750
|
+
|
|
2751
|
+
// Define a manager class
|
|
2752
|
+
class UserCardManager {
|
|
2753
|
+
element;
|
|
2754
|
+
name = '';
|
|
2755
|
+
email = '';
|
|
2756
|
+
|
|
2757
|
+
constructor(element, initVals) {
|
|
2758
|
+
this.element = element;
|
|
2759
|
+
if (initVals) {
|
|
2760
|
+
Object.assign(this, initVals);
|
|
2761
|
+
this.render();
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
render() {
|
|
2766
|
+
this.element.querySelector('[itemprop="name"]').textContent = this.name;
|
|
2767
|
+
this.element.querySelector('[itemprop="email"]').textContent = this.email;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// Register the manager
|
|
2772
|
+
customElements.itemscopeRegistry.define('user-card', {
|
|
2773
|
+
manager: UserCardManager
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
// Use assignGingerly with the 'ish' property
|
|
2777
|
+
const element = document.querySelector('[itemscope="user-card"]');
|
|
2778
|
+
element.assignGingerly({
|
|
2779
|
+
ish: {
|
|
2780
|
+
name: 'Alice',
|
|
2781
|
+
email: 'alice@example.com'
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
// Wait for async setup to complete
|
|
2786
|
+
await customElements.itemscopeRegistry.whenDefined('user-card');
|
|
2787
|
+
|
|
2788
|
+
// Access the manager instance
|
|
2789
|
+
console.log(element.ish instanceof UserCardManager); // true
|
|
2790
|
+
console.log(element.ish.name); // 'Alice'
|
|
2791
|
+
```
|
|
2792
|
+
|
|
2793
|
+
### The 'ish' Property
|
|
2794
|
+
|
|
2795
|
+
The `ish` property (short for "itemscope host") is the key to ItemScope Managers:
|
|
2796
|
+
|
|
2797
|
+
- **Special behavior for HTMLElements**: When you assign an `ish` property to an HTMLElement with an `itemscope` attribute, it triggers manager instantiation
|
|
2798
|
+
- **Normal property for other objects**: For non-HTMLElement objects, `ish` is just a regular property with no special behavior
|
|
2799
|
+
- **Asynchronous setup**: The manager is instantiated asynchronously, so use `whenDefined()` to wait for completion
|
|
2800
|
+
|
|
2801
|
+
```TypeScript
|
|
2802
|
+
// HTMLElement with itemscope - special behavior
|
|
2803
|
+
const div = document.createElement('div');
|
|
2804
|
+
div.setAttribute('itemscope', 'my-manager');
|
|
2805
|
+
div.assignGingerly({ ish: { prop: 'value' } });
|
|
2806
|
+
// Manager will be instantiated asynchronously
|
|
2807
|
+
|
|
2808
|
+
// Plain object - normal property
|
|
2809
|
+
const obj = {};
|
|
2810
|
+
obj.assignGingerly({ ish: { prop: 'value' } });
|
|
2811
|
+
console.log(obj.ish.prop); // 'value' - just a regular property
|
|
2812
|
+
```
|
|
2813
|
+
|
|
2814
|
+
### ItemscopeRegistry
|
|
2815
|
+
|
|
2816
|
+
The `ItemscopeRegistry` class manages manager configurations and extends `EventTarget` to support lazy registration:
|
|
2817
|
+
|
|
2818
|
+
```TypeScript
|
|
2819
|
+
// Access the global registry
|
|
2820
|
+
const registry = customElements.itemscopeRegistry;
|
|
2821
|
+
|
|
2822
|
+
// Define a manager
|
|
2823
|
+
registry.define('manager-name', {
|
|
2824
|
+
manager: ManagerClass,
|
|
2825
|
+
lifecycleKeys: {
|
|
2826
|
+
dispose: 'cleanup',
|
|
2827
|
+
resolved: 'isReady'
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
// Get a manager configuration
|
|
2832
|
+
const config = registry.get('manager-name');
|
|
2833
|
+
|
|
2834
|
+
// Wait for a manager to be defined and all setups to complete
|
|
2835
|
+
await registry.whenDefined('manager-name');
|
|
2836
|
+
```
|
|
2837
|
+
|
|
2838
|
+
**Methods:**
|
|
2839
|
+
|
|
2840
|
+
- `define(name, config)` - Register a manager configuration
|
|
2841
|
+
- Throws `Error: Already registered` if name already exists
|
|
2842
|
+
- Dispatches an event with the manager name when successful
|
|
2843
|
+
|
|
2844
|
+
- `get(name)` - Retrieve a manager configuration
|
|
2845
|
+
- Returns the configuration or `undefined` if not found
|
|
2846
|
+
|
|
2847
|
+
- `whenDefined(name)` - Wait for manager definition and setup completion
|
|
2848
|
+
- Returns a Promise that resolves when:
|
|
2849
|
+
1. The manager is defined (waits for definition if not yet registered)
|
|
2850
|
+
2. All pending `ish` property setups for this manager are complete
|
|
2851
|
+
- This is the recommended way to wait for async manager instantiation
|
|
2852
|
+
|
|
2853
|
+
### Manager Configuration
|
|
2854
|
+
|
|
2855
|
+
Manager configurations follow this interface:
|
|
2856
|
+
|
|
2857
|
+
```TypeScript
|
|
2858
|
+
interface ItemscopeManagerConfig<T = any> {
|
|
2859
|
+
manager: {
|
|
2860
|
+
new (element: HTMLElement, initVals?: Partial<T>): T;
|
|
2861
|
+
};
|
|
2862
|
+
lifecycleKeys?: {
|
|
2863
|
+
dispose?: string | symbol;
|
|
2864
|
+
resolved?: string | symbol;
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
```
|
|
2868
|
+
|
|
2869
|
+
**Properties:**
|
|
2870
|
+
|
|
2871
|
+
- `manager` (required): Constructor function that receives:
|
|
2872
|
+
- `element`: The HTMLElement with the itemscope attribute
|
|
2873
|
+
- `initVals`: Merged values from all queued `ish` assignments
|
|
2874
|
+
|
|
2875
|
+
- `lifecycleKeys` (optional): Lifecycle method names
|
|
2876
|
+
- `dispose`: Method to call when cleaning up
|
|
2877
|
+
- `resolved`: Property/event name for async initialization
|
|
2878
|
+
|
|
2879
|
+
### Lazy Registration
|
|
2880
|
+
|
|
2881
|
+
Managers can be registered after elements are already using them. The system queues values and instantiates the manager when it's registered:
|
|
2882
|
+
|
|
2883
|
+
```TypeScript
|
|
2884
|
+
const element = document.createElement('div');
|
|
2885
|
+
element.setAttribute('itemscope', 'lazy-manager');
|
|
2886
|
+
|
|
2887
|
+
// Assign before manager is registered - values are queued
|
|
2888
|
+
element.assignGingerly({ ish: { prop1: 'value1' } });
|
|
2889
|
+
element.assignGingerly({ ish: { prop2: 'value2' } });
|
|
2890
|
+
|
|
2891
|
+
// Register the manager later
|
|
2892
|
+
setTimeout(() => {
|
|
2893
|
+
customElements.itemscopeRegistry.define('lazy-manager', {
|
|
2894
|
+
manager: class LazyManager {
|
|
2895
|
+
constructor(element, initVals) {
|
|
2896
|
+
this.element = element;
|
|
2897
|
+
Object.assign(this, initVals);
|
|
2898
|
+
// initVals contains both prop1 and prop2
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
}, 100);
|
|
2903
|
+
|
|
2904
|
+
// Wait for registration and setup
|
|
2905
|
+
await customElements.itemscopeRegistry.whenDefined('lazy-manager');
|
|
2906
|
+
|
|
2907
|
+
console.log(element.ish.prop1); // 'value1'
|
|
2908
|
+
console.log(element.ish.prop2); // 'value2'
|
|
2909
|
+
```
|
|
2910
|
+
|
|
2911
|
+
### Instance Caching
|
|
2912
|
+
|
|
2913
|
+
Manager instances are cached per element. Subsequent `ish` assignments merge values into the existing instance:
|
|
2914
|
+
|
|
2915
|
+
```TypeScript
|
|
2916
|
+
const element = document.createElement('div');
|
|
2917
|
+
element.setAttribute('itemscope', 'my-manager');
|
|
2918
|
+
|
|
2919
|
+
// First assignment - creates instance
|
|
2920
|
+
element.assignGingerly({ ish: { prop1: 'value1' } });
|
|
2921
|
+
await customElements.itemscopeRegistry.whenDefined('my-manager');
|
|
2922
|
+
|
|
2923
|
+
const firstInstance = element.ish;
|
|
2924
|
+
|
|
2925
|
+
// Second assignment - reuses instance
|
|
2926
|
+
element.assignGingerly({ ish: { prop2: 'value2' } });
|
|
2927
|
+
await customElements.itemscopeRegistry.whenDefined('my-manager');
|
|
2928
|
+
|
|
2929
|
+
console.log(element.ish === firstInstance); // true - same instance
|
|
2930
|
+
console.log(element.ish.prop1); // 'value1'
|
|
2931
|
+
console.log(element.ish.prop2); // 'value2'
|
|
2932
|
+
```
|
|
2933
|
+
|
|
2934
|
+
### Validation and Error Handling
|
|
2935
|
+
|
|
2936
|
+
The system validates `ish` property assignments and throws descriptive errors:
|
|
2937
|
+
|
|
2938
|
+
```TypeScript
|
|
2939
|
+
// Error: Element must have itemscope attribute
|
|
2940
|
+
const div1 = document.createElement('div');
|
|
2941
|
+
div1.assignGingerly({ ish: { prop: 'value' } });
|
|
2942
|
+
// Throws asynchronously
|
|
2943
|
+
|
|
2944
|
+
// Error: itemscope must be non-empty string
|
|
2945
|
+
const div2 = document.createElement('div');
|
|
2946
|
+
div2.setAttribute('itemscope', '');
|
|
2947
|
+
div2.assignGingerly({ ish: { prop: 'value' } });
|
|
2948
|
+
// Throws asynchronously
|
|
2949
|
+
|
|
2950
|
+
// Error: ish value must be an object
|
|
2951
|
+
const div3 = document.createElement('div');
|
|
2952
|
+
div3.setAttribute('itemscope', 'my-manager');
|
|
2953
|
+
div3.assignGingerly({ ish: 'string' });
|
|
2954
|
+
// Throws asynchronously
|
|
2955
|
+
```
|
|
2956
|
+
|
|
2957
|
+
**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.
|
|
2958
|
+
|
|
2959
|
+
### Scoped Registries
|
|
2960
|
+
|
|
2961
|
+
ItemScope Managers integrate with scoped custom element registries. Each element can have its own registry:
|
|
2962
|
+
|
|
2963
|
+
```TypeScript
|
|
2964
|
+
// Create a scoped registry
|
|
2965
|
+
const scopedRegistry = new CustomElementRegistry();
|
|
2966
|
+
|
|
2967
|
+
// Define a manager in the scoped registry
|
|
2968
|
+
scopedRegistry.itemscopeRegistry.define('scoped-manager', {
|
|
2969
|
+
manager: ScopedManager
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
// Attach the registry to an element
|
|
2973
|
+
const element = document.createElement('div');
|
|
2974
|
+
element.customElementRegistry = scopedRegistry;
|
|
2975
|
+
element.setAttribute('itemscope', 'scoped-manager');
|
|
2976
|
+
|
|
2977
|
+
// The element uses its scoped registry
|
|
2978
|
+
element.assignGingerly({ ish: { prop: 'value' } });
|
|
2979
|
+
await scopedRegistry.itemscopeRegistry.whenDefined('scoped-manager');
|
|
2980
|
+
```
|
|
2981
|
+
|
|
2982
|
+
If an element doesn't have a `customElementRegistry` property, it falls back to the global `customElements.itemscopeRegistry`.
|
|
2983
|
+
|
|
2984
|
+
### Complete Example
|
|
2985
|
+
|
|
2986
|
+
```html
|
|
2987
|
+
<!DOCTYPE html>
|
|
2988
|
+
<html>
|
|
2989
|
+
<head>
|
|
2990
|
+
<script type="module">
|
|
2991
|
+
import 'assign-gingerly/object-extension.js';
|
|
2992
|
+
|
|
2993
|
+
// Define a todo item manager
|
|
2994
|
+
class TodoItemManager {
|
|
2995
|
+
element;
|
|
2996
|
+
text = '';
|
|
2997
|
+
completed = false;
|
|
2998
|
+
|
|
2999
|
+
constructor(element, initVals) {
|
|
3000
|
+
this.element = element;
|
|
3001
|
+
if (initVals) {
|
|
3002
|
+
Object.assign(this, initVals);
|
|
3003
|
+
}
|
|
3004
|
+
this.render();
|
|
3005
|
+
this.attachListeners();
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
render() {
|
|
3009
|
+
const checkbox = this.element.querySelector('input[type="checkbox"]');
|
|
3010
|
+
const label = this.element.querySelector('label');
|
|
3011
|
+
|
|
3012
|
+
if (checkbox) checkbox.checked = this.completed;
|
|
3013
|
+
if (label) label.textContent = this.text;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
attachListeners() {
|
|
3017
|
+
const checkbox = this.element.querySelector('input[type="checkbox"]');
|
|
3018
|
+
if (checkbox) {
|
|
3019
|
+
checkbox.addEventListener('change', (e) => {
|
|
3020
|
+
this.completed = e.target.checked;
|
|
3021
|
+
this.render();
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
cleanup() {
|
|
3027
|
+
// Remove event listeners, etc.
|
|
3028
|
+
console.log('Cleaning up todo item');
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Register the manager
|
|
3033
|
+
customElements.itemscopeRegistry.define('todo-item', {
|
|
3034
|
+
manager: TodoItemManager,
|
|
3035
|
+
lifecycleKeys: {
|
|
3036
|
+
dispose: 'cleanup'
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
// Initialize todo items
|
|
3041
|
+
async function initTodos() {
|
|
3042
|
+
const items = document.querySelectorAll('[itemscope="todo-item"]');
|
|
3043
|
+
|
|
3044
|
+
items.forEach((item, index) => {
|
|
3045
|
+
item.assignGingerly({
|
|
3046
|
+
ish: {
|
|
3047
|
+
text: `Todo item ${index + 1}`,
|
|
3048
|
+
completed: false
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
});
|
|
3052
|
+
|
|
3053
|
+
// Wait for all setups to complete
|
|
3054
|
+
await customElements.itemscopeRegistry.whenDefined('todo-item');
|
|
3055
|
+
|
|
3056
|
+
console.log('All todo items initialized');
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// Run on page load
|
|
3060
|
+
document.addEventListener('DOMContentLoaded', initTodos);
|
|
3061
|
+
</script>
|
|
3062
|
+
</head>
|
|
3063
|
+
<body>
|
|
3064
|
+
<h1>Todo List</h1>
|
|
3065
|
+
<ul>
|
|
3066
|
+
<li itemscope="todo-item">
|
|
3067
|
+
<input type="checkbox">
|
|
3068
|
+
<label></label>
|
|
3069
|
+
</li>
|
|
3070
|
+
<li itemscope="todo-item">
|
|
3071
|
+
<input type="checkbox">
|
|
3072
|
+
<label></label>
|
|
3073
|
+
</li>
|
|
3074
|
+
<li itemscope="todo-item">
|
|
3075
|
+
<input type="checkbox">
|
|
3076
|
+
<label></label>
|
|
3077
|
+
</li>
|
|
3078
|
+
</ul>
|
|
3079
|
+
</body>
|
|
3080
|
+
</html>
|
|
3081
|
+
```
|
|
3082
|
+
|
|
3083
|
+
### Testing with whenDefined
|
|
3084
|
+
|
|
3085
|
+
When writing tests for code that uses ItemScope Managers, use `whenDefined()` to wait for async setup:
|
|
3086
|
+
|
|
3087
|
+
```TypeScript
|
|
3088
|
+
// Test example
|
|
3089
|
+
test('should initialize manager with values', async () => {
|
|
3090
|
+
const element = document.createElement('div');
|
|
3091
|
+
element.setAttribute('itemscope', 'test-manager');
|
|
3092
|
+
|
|
3093
|
+
// Register manager
|
|
3094
|
+
customElements.itemscopeRegistry.define('test-manager', {
|
|
3095
|
+
manager: class TestManager {
|
|
3096
|
+
constructor(element, initVals) {
|
|
3097
|
+
this.element = element;
|
|
3098
|
+
Object.assign(this, initVals);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
|
|
3103
|
+
// Assign values
|
|
3104
|
+
element.assignGingerly({ ish: { prop: 'value' } });
|
|
3105
|
+
|
|
3106
|
+
// Wait for setup to complete
|
|
3107
|
+
await customElements.itemscopeRegistry.whenDefined('test-manager');
|
|
3108
|
+
|
|
3109
|
+
// Now we can assert
|
|
3110
|
+
expect(element.ish.prop).toBe('value');
|
|
3111
|
+
expect(element.ish.element).toBe(element);
|
|
3112
|
+
});
|
|
3113
|
+
```
|
|
3114
|
+
|
|
3115
|
+
### Design Rationale
|
|
3116
|
+
|
|
3117
|
+
ItemScope Managers follow these design principles:
|
|
3118
|
+
|
|
3119
|
+
1. **Synchronous API**: `assignGingerly` remains synchronous and returns immediately
|
|
3120
|
+
2. **Async setup**: Manager instantiation happens asynchronously in the background
|
|
3121
|
+
3. **Explicit waiting**: Use `whenDefined()` when you need to wait for setup completion
|
|
3122
|
+
4. **Dual behavior**: The `ish` property has special meaning only for HTMLElements with `itemscope` attributes
|
|
3123
|
+
5. **Registry-based**: Follows the same pattern as `EnhancementRegistry` for consistency
|
|
3124
|
+
6. **Event-driven**: Uses EventTarget for lazy registration support
|
|
3125
|
+
|
|
3126
|
+
This design ensures backward compatibility while providing powerful new capabilities for managing DOM fragments.
|
|
3127
|
+
|
|
2455
3128
|
|
|
2456
3129
|
|
|
2457
3130
|
|