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 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
- 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.
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
- ## Custom Enhancement Registry
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
- On top of that, this polyfill package builds on the newly minted Custom Element Registry, adding an additional EnhancementRegistry object on top of the customElementRegistry object associated with all elements, to be able to manage namespace conflicts, and, importantly, as a basis for defining custom attributes associated with the enhancements.
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 you to delete properties from objects:
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
- '?.style?.height': '40px',
348
- '?.enh?.mellowYellow?.madAboutFourteen': true
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
- '?.style.height': '40px',
466
- '?.enh?.mellowYellow?.madAboutFourteen': true
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 Integration (Chrome 146+)
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
- This package includes support for Chrome's scoped custom element registries, which automatically integrates dependency injection in harmony with scoped custom elements DOM sections or ShadowRoots.
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 you to assign properties to enhancements using a clean, chainable syntax:
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 instanted using new .... Arrow functions cannot be used.
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
- **Example without enhKey:**
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
- **How it works:**
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
- **Note**: `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.
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?: (v: string | null) => any; // Custom parser function
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 also reference static methods on custom elements using dot notation:
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.parseSpecialFormat' // element-name.methodName
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 Order:**
2048
+ **Parser Resolution:**
1800
2049
 
1801
- When a string parser is specified:
2050
+ When a parser is specified, it can be:
1802
2051
 
1803
- 1. **Check for dot notation** - If parser contains `.`, try to resolve as `element-name.methodName`
1804
- 2. **Try custom element** - Look up element in `customElements` registry and check for static method
1805
- 3. **Fall back to global registry** - If custom element not found, check `globalParserRegistry`
1806
- 4. **Throw error** - If not found anywhere, throw descriptive error
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
- This allows:
1809
- - Element-specific parsers to be scoped to their custom elements
1810
- - Fallback to global registry for shared parsers
1811
- - Dot notation in global registry names (e.g., `'utils.parseDate'`)
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 you to specify default values when attributes are missing:
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 query = buildCSSQuery(enhancementConfig);
2361
- const observer = new MutationObserver(() => {
2362
- const elements = document.querySelectorAll(query);
2363
- elements.forEach(el => enhance(el));
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