assign-gingerly 0.0.31 → 0.0.33

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
@@ -110,7 +110,7 @@ console.log(obj);
110
110
 
111
111
  When the right hand side of an expression is an object, assignGingerly behavior depends on the context:
112
112
  - For **nested paths** (starting with `?.`): recursively merges into nested objects, creating them if needed
113
- - For **plain keys**: performs simple assignment (like `Object.assign`), unless the target property is readonly or a class instance (see Examples 3a and 3b below)
113
+ - For **plain keys**: performs simple assignment (like `Object.assign`), unless the target property is readonly or an accessor (see Examples 3a and 3b below)
114
114
 
115
115
  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.
116
116
 
@@ -144,38 +144,40 @@ console.log(obj.config); // { theme: 'dark' } - intermediate object created
144
144
 
145
145
  ## Example 3a - Automatic Readonly Property Detection
146
146
 
147
- 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:
147
+ assignGingerly automatically detects readonly properties and merges into them instead of attempting to replace them. This makes working with DOM properties like `dataset` ergonomic:
148
148
 
149
149
  ```TypeScript
150
- // Instead of this verbose syntax:
151
150
  const div = document.createElement('div');
152
151
  assignGingerly(div, {
153
- '?.style?.height': '15px',
154
- '?.style?.width': '20px'
155
- });
156
-
157
- // You can now use this cleaner syntax:
158
- assignGingerly(div, {
159
- style: {
160
- height: '15px',
161
- width: '20px'
152
+ dataset: {
153
+ userId: '123',
154
+ userName: 'Alice'
162
155
  }
163
156
  });
164
- console.log(div.style.height); // '15px'
165
- console.log(div.style.width); // '20px'
157
+ console.log(div.dataset.userId); // '123'
158
+ console.log(div.dataset.userName); // 'Alice'
166
159
  ```
167
160
 
168
161
  **How it works:**
169
162
 
170
163
  When assignGingerly encounters an object value being assigned to an existing property, it checks if that property is readonly:
171
164
  - **Data properties** with `writable: false`
172
- - **Accessor properties** with a getter but no setter
165
+ - **Accessor properties** with a getter but no setter (e.g., `dataset`, `shadowRoot`)
173
166
 
174
167
  If the property is readonly and its current value is an object, assignGingerly automatically merges into it recursively.
175
168
 
176
- **Examples of readonly properties:**
177
- - `HTMLElement.style` - The CSSStyleDeclaration object
178
- - `HTMLElement.dataset` - The DOMStringMap object
169
+ **Note on `element.style`:** The `style` property has both a getter and a setter, so it is *not* treated as readonly. Use nested path syntax instead:
170
+
171
+ ```TypeScript
172
+ // Use nested path syntax for style
173
+ assignGingerly(div, {
174
+ '?.style?.height': '15px',
175
+ '?.style?.width': '20px'
176
+ });
177
+ ```
178
+
179
+ **Examples of readonly properties that trigger merging:**
180
+ - `HTMLElement.dataset` - getter only, no setter
179
181
  - Custom objects with `Object.defineProperty(obj, 'prop', { value: {}, writable: false })`
180
182
  - Accessor properties with getter only: `Object.defineProperty(obj, 'prop', { get() { return {}; } })`
181
183
 
@@ -225,134 +227,54 @@ assignGingerly(config, {
225
227
  console.log(config.settings.theme); // 'dark'
226
228
  ```
227
229
 
228
- ## Example 3b - Automatic Class Instance Preservation
230
+ ## Example 3b - Class Instances Are Replaced
229
231
 
230
- In addition to readonly property detection, assignGingerly automatically preserves class instances when merging. This is particularly useful when working with enhancement instances:
232
+ Unlike readonly/accessor properties, class instances on writable properties are **replaced** by simple assignment, just like plain objects. This allows you to swap one object for another without unexpected merging:
231
233
 
232
234
  ```TypeScript
233
- import 'assign-gingerly/object-extension.js';
234
-
235
- // Define an enhancement class
236
- class MyEnhancement {
237
- constructor(element, ctx, initVals) {
238
- this.element = element;
239
- this.instanceId = Math.random(); // Track instance identity
240
- if (initVals) {
241
- Object.assign(this, initVals);
242
- }
235
+ class FakeDocumentFragment {
236
+ constructor() {
237
+ this.nodeType = 11;
238
+ this.childNodes = [];
243
239
  }
244
- prop1 = null;
245
- prop2 = null;
246
240
  }
247
241
 
248
- const element = document.createElement('div');
249
- element.enh = {
250
- myEnh: new MyEnhancement(element, {}, {})
242
+ const obj = {
243
+ clone: new FakeDocumentFragment()
251
244
  };
252
245
 
253
- const originalId = element.enh.myEnh.instanceId;
254
-
255
- // Clean syntax - no need for ?.myEnh?.prop1 notation
256
- assignGingerly(element, {
257
- enh: {
258
- myEnh: {
259
- prop1: 'value1',
260
- prop2: 'value2'
261
- }
262
- }
263
- });
264
-
265
- console.log(element.enh.myEnh.instanceId === originalId); // true - instance preserved!
266
- console.log(element.enh.myEnh.prop1); // 'value1'
267
- console.log(element.enh.myEnh.prop2); // 'value2'
268
- ```
269
-
270
- **How it works:**
271
-
272
- 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):
273
-
274
- - **Class instances** are detected by checking if their prototype is something other than `Object.prototype` or `null`
275
- - **Plain objects** `{}` have `Object.prototype` as their prototype
276
- - **Class instances** have their class's prototype
277
-
278
- If the existing value is a class instance, assignGingerly merges into it instead of replacing it.
279
-
280
- **What counts as a class instance:**
281
- - Custom class instances: `new MyClass()`
282
- - Built-in class instances: `new Date()`, `new Map()`, `new Set()`, etc.
283
- - Enhancement instances on the `enh` property
284
- - Any object whose prototype is not `Object.prototype` or `null`
285
-
286
- **What doesn't count:**
287
- - Plain objects: `{}`, `{ a: 1 }`
288
- - Arrays: `[]`, `[1, 2, 3]` (arrays are replaced, not merged)
289
- - Primitives: strings, numbers, booleans
290
-
291
- **Benefits:**
292
-
293
- This feature enables clean, framework-friendly syntax for updating enhancements:
246
+ const element = document.createElement('div');
294
247
 
295
- ```TypeScript
296
- // Before: Verbose nested path syntax
297
- assignGingerly(element, {
298
- '?.enh?.mellowYellow?.madAboutFourteen': true
248
+ // Replace the DocumentFragment with the actual element
249
+ assignGingerly(obj, {
250
+ clone: element
299
251
  });
300
252
 
301
- // After: Clean object syntax
302
- assignGingerly(element, {
303
- enh: {
304
- mellowYellow: {
305
- madAboutFourteen: true
306
- }
307
- }
308
- });
253
+ console.log(obj.clone === element); // true - replaced, not merged
309
254
  ```
310
255
 
311
- **Additional examples:**
312
-
313
- ```TypeScript
314
- // Multiple enhancements at once
315
- assignGingerly(element, {
316
- enh: {
317
- enhancement1: { prop: 'value1' },
318
- enhancement2: { prop: 'value2' }
319
- }
320
- });
321
-
322
- // Works with built-in classes too
323
- const obj = {
324
- timestamp: new Date('2024-01-01')
325
- };
326
-
327
- assignGingerly(obj, {
328
- timestamp: {
329
- customProp: 'metadata'
330
- }
331
- });
256
+ **Why replacement instead of merging?**
332
257
 
333
- console.log(obj.timestamp instanceof Date); // true - Date instance preserved
334
- console.log(obj.timestamp.customProp); // 'metadata'
335
- ```
258
+ In real-world use cases, you often need to replace one object with another of a completely different type. For example, replacing a cloned DocumentFragment with the actual web component element. Automatic merging would corrupt the target by mixing properties from incompatible types.
336
259
 
337
- **Combined with readonly detection:**
260
+ **Readonly/accessor properties are still merged:**
338
261
 
339
- Both readonly properties and class instances are preserved:
262
+ The distinction is clear:
263
+ - **Writable data properties**: always replaced (whether holding a plain object or class instance)
264
+ - **Readonly data properties** (`writable: false`): merged into
265
+ - **Getter-only accessor properties** (no setter): merged into
266
+ - **Getter+setter accessor properties** (e.g., `style`): setter runs with the value as-is
340
267
 
341
268
  ```TypeScript
342
269
  const div = document.createElement('div');
343
- div.enh = {
344
- myEnh: new MyEnhancement(div, {}, {})
345
- };
346
270
 
347
271
  assignGingerly(div, {
348
- style: { height: '100px' }, // Readonly - merged
349
- enh: {
350
- myEnh: { prop: 'value' } // Class instance - merged
351
- },
352
- dataset: { userId: '123' } // Readonly - merged
272
+ dataset: { userId: '123' }, // Getter-only - merged
273
+ '?.style?.height': '100px' // Use nested path for style
353
274
  });
354
275
 
355
- // All instances and readonly objects preserved
276
+ console.log(div.dataset.userId); // '123'
277
+ console.log(div.style.height); // '100px'
356
278
  ```
357
279
 
358
280
  ## Example 3c - Method Calls with withMethods
@@ -411,25 +333,31 @@ assignGingerly(elementRef, {
411
333
  // Equivalent to: elementRef.deref().classList.add('active')
412
334
  ```
413
335
 
414
- **Complex chaining:**
336
+ **Complex chaining with real DOM elements:**
337
+
338
+ Methods are called on the objects found through chained accessors, not just on the root object:
415
339
 
416
340
  ```TypeScript
417
- const shadowRoot = {
418
- querySelector(selector) {
419
- return this.elements[selector];
420
- },
421
- elements: {
422
- 'my-element': document.createElement('div')
423
- }
424
- };
341
+ const div = document.createElement('div');
342
+ div.innerHTML = `
343
+ <my-element>
344
+ <your-element></your-element>
345
+ </my-element>
346
+ `;
425
347
 
426
- assignGingerly(shadowRoot, {
427
- '?.querySelector?.my-element?.classList?.add': 'highlighted'
348
+ assignGingerly(div, {
349
+ '?.querySelector?.my-element?.querySelector?.your-element?.classList?.add': 'highlighted'
428
350
  }, { withMethods: ['querySelector', 'add'] });
429
351
 
430
- // Equivalent to: shadowRoot.querySelector('my-element').classList.add('highlighted')
352
+ // Equivalent to:
353
+ // div.querySelector('my-element').querySelector('your-element').classList.add('highlighted')
354
+
355
+ const yourElement = div.querySelector('my-element')?.querySelector('your-element');
356
+ console.log(yourElement?.classList.contains('highlighted')); // true
431
357
  ```
432
358
 
359
+ The key insight: `querySelector` is called on each intermediate result in the chain. First on `div`, then on the `my-element` result, demonstrating that methods work naturally with the object hierarchy you're navigating.
360
+
433
361
  **Using Set for withMethods:**
434
362
 
435
363
  For better performance with many methods, use a Set:
@@ -464,6 +392,321 @@ assignGingerly(element, {
464
392
  - Silent failure for non-existent methods (garbage in, garbage out)
465
393
  - Supports method chaining and complex navigation patterns
466
394
 
395
+ ## Example 3d - Aliasing with aka
396
+
397
+ The `aka` option allows you to define custom shortcuts (aliases) for property and method names, reducing verbosity in repetitive patterns. This is inspired by jQuery's `$` shortcut for `querySelectorAll`, but fully customizable.
398
+
399
+ ```TypeScript
400
+ import assignGingerly from 'assign-gingerly';
401
+
402
+ const div = document.createElement('div');
403
+ div.innerHTML = `
404
+ <my-element>
405
+ <your-element></your-element>
406
+ </my-element>
407
+ `;
408
+
409
+ // Without aliases (verbose)
410
+ assignGingerly(div, {
411
+ '?.querySelector?.my-element?.classList?.add': 'highlighted',
412
+ '?.querySelector?.your-element?.classList?.add': 'active'
413
+ }, { withMethods: ['querySelector', 'add'] });
414
+
415
+ // With aliases (concise)
416
+ assignGingerly(div, {
417
+ '?.$?.my-element?.c?.+': 'highlighted',
418
+ '?.$?.your-element?.c?.+': 'active'
419
+ }, {
420
+ withMethods: ['querySelector', 'add'],
421
+ aka: { '$': 'querySelector', 'c': 'classList', '+': 'add' }
422
+ });
423
+ ```
424
+
425
+ **How it works:**
426
+
427
+ - Aliases are substituted **before** path evaluation
428
+ - Matches complete tokens between `?.` delimiters (not substrings)
429
+ - Works for both properties and methods
430
+ - Single or multi-character aliases supported
431
+
432
+ **Reserved characters:**
433
+
434
+ Cannot be used in aliases: space (` `), backtick (`` ` ``)
435
+
436
+ **Multi-character aliases:**
437
+
438
+ ```TypeScript
439
+ assignGingerly(element, {
440
+ '?.qs?.my-element?.cl?.add': 'highlighted'
441
+ }, {
442
+ withMethods: ['querySelector', 'add'],
443
+ aka: { 'qs': 'querySelector', 'cl': 'classList' }
444
+ });
445
+ ```
446
+
447
+ **Multiple aliases in one path:**
448
+
449
+ ```TypeScript
450
+ assignGingerly(element, {
451
+ '?.c?.+': 'class1',
452
+ '?.p?.+': 'part1',
453
+ '?.ds?.userId': '123'
454
+ }, {
455
+ withMethods: ['add'],
456
+ aka: {
457
+ 'c': 'classList',
458
+ 'p': 'part',
459
+ 'ds': 'dataset',
460
+ '+': 'add'
461
+ }
462
+ });
463
+
464
+ // Equivalent to:
465
+ // element.classList.add('class1')
466
+ // element.part.add('part1')
467
+ // element.dataset.userId = '123'
468
+ ```
469
+
470
+ **Benefits:**
471
+
472
+ - Reduces verbosity in repetitive patterns
473
+ - Fully customizable shortcuts
474
+ - Improves readability when you have many similar operations
475
+ - Works seamlessly with `withMethods`
476
+
477
+ ## Example 3e - ForEach with @each
478
+
479
+ The `@each` symbol allows you to iterate over collections and apply operations to each item. This works with any iterable including Arrays, NodeList, HTMLCollection, and more.
480
+
481
+ ```TypeScript
482
+ import assignGingerly from 'assign-gingerly';
483
+
484
+ const div = document.createElement('div');
485
+ div.innerHTML = `
486
+ <my-element></my-element>
487
+ <my-element></my-element>
488
+ <my-element></my-element>
489
+ `;
490
+
491
+ // Apply to each element in the collection
492
+ assignGingerly(div, {
493
+ '?.querySelectorAll?.my-element?.@each?.classList?.add': 'highlighted'
494
+ }, { withMethods: ['querySelectorAll', 'add'] });
495
+
496
+ // All my-element elements now have the 'highlighted' class
497
+ ```
498
+
499
+ **How it works:**
500
+
501
+ - `@each` marks the point where iteration begins
502
+ - Everything before `@each` navigates to the iterable
503
+ - Everything after `@each` is applied to each item in the collection
504
+ - Empty collections are handled gracefully (no errors)
505
+
506
+ **With regular arrays:**
507
+
508
+ ```TypeScript
509
+ const obj = {
510
+ items: [
511
+ { value: null },
512
+ { value: null },
513
+ { value: null }
514
+ ]
515
+ };
516
+
517
+ assignGingerly(obj, {
518
+ '?.items?.@each?.value': 'test'
519
+ });
520
+
521
+ // All items now have value: 'test'
522
+ ```
523
+
524
+ **Nested forEach:**
525
+
526
+ ```TypeScript
527
+ const obj = {
528
+ groups: [
529
+ { items: [{ value: null }, { value: null }] },
530
+ { items: [{ value: null }, { value: null }] }
531
+ ]
532
+ };
533
+
534
+ assignGingerly(obj, {
535
+ '?.groups?.@each?.items?.@each?.value': 'nested'
536
+ });
537
+
538
+ // All nested items now have value: 'nested'
539
+ ```
540
+
541
+ **With aliases:**
542
+
543
+ ```TypeScript
544
+ assignGingerly(div, {
545
+ '?.qsa?.my-element?.*?.c?.+': 'highlighted'
546
+ }, {
547
+ withMethods: ['querySelectorAll', 'add'],
548
+ aka: {
549
+ 'qsa': 'querySelectorAll',
550
+ 'c': 'classList',
551
+ '+': 'add',
552
+ '*': '@each' // Alias * to @each for brevity
553
+ }
554
+ });
555
+ ```
556
+
557
+ **Method calls on each item:**
558
+
559
+ ```TypeScript
560
+ assignGingerly(div, {
561
+ '?.querySelectorAll?.div?.@each?.setAttribute': ['data-id', '123']
562
+ }, { withMethods: ['querySelectorAll', 'setAttribute'] });
563
+
564
+ // All div elements now have data-id="123"
565
+ ```
566
+
567
+ **Accessing iterable properties:**
568
+
569
+ When you omit `@each`, you access properties on the iterable itself, not its items:
570
+
571
+ ```TypeScript
572
+ const obj = {
573
+ items: [1, 2, 3],
574
+ customProp: null
575
+ };
576
+
577
+ // Set property on the array itself
578
+ assignGingerly(obj, {
579
+ '?.items?.customProp': 'test'
580
+ });
581
+
582
+ console.log(obj.items.customProp); // 'test'
583
+ ```
584
+
585
+ **Benefits:**
586
+
587
+ - Works with any iterable (Arrays, NodeList, HTMLCollection, etc.)
588
+ - Supports nested iterations
589
+ - Integrates seamlessly with `withMethods` and `aka`
590
+ - Clear distinction between iterating and accessing iterable properties
591
+ - Graceful handling of empty collections
592
+
593
+ ## Example 3f - Reactive Iteration with @eachTime
594
+
595
+ The `@eachTime` symbol enables reactive iteration over elements as they mount or appear dynamically. Unlike `@each` which operates on static collections, `@eachTime` subscribes to events and applies operations to elements as they arrive over time.
596
+
597
+ **Important:** This feature requires an `AbortSignal` for cleanup and is designed to work with EventTarget objects that emit 'mount' events (such as [mount-observer](https://github.com/bahrus/mount-observer)).
598
+
599
+ ```TypeScript
600
+ import assignGingerly from 'assign-gingerly';
601
+
602
+ const controller = new AbortController();
603
+ const div = document.createElement('div');
604
+
605
+ // Assume mountObserver is an IMountObserver instance that emits 'mount' events
606
+ // when new elements matching 'my-element' are added to the DOM
607
+
608
+ assignGingerly(div, {
609
+ '?.mountObserver?.@eachTime?.classList?.add': 'highlighted'
610
+ }, {
611
+ withMethods: ['add'],
612
+ signal: controller.signal // Required for cleanup
613
+ });
614
+
615
+ // As elements mount, they automatically get the 'highlighted' class
616
+ // Later, cleanup all listeners:
617
+ controller.abort();
618
+ ```
619
+
620
+ **How it works:**
621
+
622
+ - `@eachTime` marks the point where reactive iteration begins
623
+ - Everything before `@eachTime` must navigate to an EventTarget
624
+ - The EventTarget must emit 'mount' events with a `mountedElement` property
625
+ - Everything after `@eachTime` is applied to each mounted element
626
+ - Event listeners are automatically cleaned up when the AbortSignal is aborted
627
+
628
+ **With method calls:**
629
+
630
+ ```TypeScript
631
+ const controller = new AbortController();
632
+
633
+ assignGingerly(div, {
634
+ '?.mountObserver?.@eachTime?.setAttribute': ['data-mounted', 'true']
635
+ }, {
636
+ withMethods: ['setAttribute'],
637
+ signal: controller.signal
638
+ });
639
+
640
+ // Each mounted element gets data-mounted="true"
641
+ ```
642
+
643
+ **With aliases:**
644
+
645
+ ```TypeScript
646
+ const controller = new AbortController();
647
+
648
+ assignGingerly(div, {
649
+ '?.mo?.@*?.c?.+': 'active'
650
+ }, {
651
+ withMethods: ['add'],
652
+ aka: {
653
+ 'mo': 'mountObserver',
654
+ '@*': '@eachTime',
655
+ 'c': 'classList',
656
+ '+': 'add'
657
+ },
658
+ signal: controller.signal
659
+ });
660
+ ```
661
+
662
+ **Cleanup is required:**
663
+
664
+ ```TypeScript
665
+ const controller = new AbortController();
666
+
667
+ // Setup reactive iteration
668
+ assignGingerly(div, {
669
+ '?.mountObserver?.@eachTime?.classList?.add': 'mounted'
670
+ }, {
671
+ withMethods: ['add'],
672
+ signal: controller.signal
673
+ });
674
+
675
+ // Later, when you're done observing:
676
+ controller.abort(); // Removes all event listeners
677
+
678
+ // Attempting to use @eachTime without a signal throws an error:
679
+ assignGingerly(div, {
680
+ '?.mountObserver?.@eachTime?.classList?.add': 'mounted'
681
+ }, { withMethods: ['add'] });
682
+ // Error: @eachTime requires an AbortSignal in options.signal for cleanup
683
+ ```
684
+
685
+ **Key differences from @each:**
686
+
687
+ | Feature | @each | @eachTime |
688
+ |---------|-------|-----------|
689
+ | **Type** | Static iteration | Reactive iteration |
690
+ | **Timing** | Immediate (synchronous) | Over time (asynchronous) |
691
+ | **Use case** | Existing collections | Elements appearing dynamically |
692
+ | **Cleanup** | Not needed | Required (AbortSignal) |
693
+ | **Requirements** | Any iterable | EventTarget with 'mount' events |
694
+
695
+ **Benefits:**
696
+
697
+ - Declarative reactive programming without RxJS complexity
698
+ - Automatic cleanup via standard AbortSignal API
699
+ - JSON-serializable configuration (behavior is in implementation)
700
+ - Fire-and-forget async pattern (doesn't block)
701
+ - Minimal weight impact (~3% when not used, dynamically loaded when needed)
702
+
703
+ **Limitations:**
704
+
705
+ - Requires EventTarget that emits 'mount' events
706
+ - AbortSignal is mandatory for cleanup
707
+ - Testing is done in mount-observer package (no tests in assign-gingerly)
708
+ - Single @eachTime per path (nested @eachTime not currently supported)
709
+
467
710
  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.
468
711
 
469
712
  ## Example 4 - Incrementing values with += command
@@ -649,10 +892,12 @@ console.log(string1 === string2); // true
649
892
 
650
893
  This guarantees that applying the reversal object restores the object to its exact original state.
651
894
 
895
+ # Object and Element Enhancements via assign-gingerly
896
+
652
897
  ## Dependency injection based on a registry object and a Symbolic reference mapping
653
898
 
654
899
  ```Typescript
655
- interface IEnhancementRegistryItem<T = any, TObjToExtend = any> {
900
+ interface EnhancementConfig<T = any, TObjToExtend = any> {
656
901
  spawn: {new(objToExtend: TObjToExtend, ctx: SpawnContext, initVals: Partial<T>): T}
657
902
  symlinks?: {[key: symbol]: keyof T}
658
903
  // Optional: for element enhancement access
@@ -678,7 +923,7 @@ class YourEnhancement{
678
923
  }
679
924
 
680
925
  class EnhancementRegistry{
681
- push(IEnhancementRegistryItem | IEnhancementRegistryItem[]){
926
+ push(EnhancementConfig | EnhancementConfig[]){
682
927
  ...
683
928
  }
684
929
  }
@@ -1035,7 +1280,7 @@ Element enhancement classes should follow this constructor signature:
1035
1280
 
1036
1281
  ```TypeScript
1037
1282
  interface SpawnContext<T, TMountContext = any> {
1038
- config: IEnhancementRegistryItem<T>;
1283
+ config: EnhancementConfig<T>;
1039
1284
  mountCtx?: TMountContext; // Optional custom context passed by caller
1040
1285
  }
1041
1286
 
@@ -1089,7 +1334,7 @@ This is useful for:
1089
1334
  In addition to spawn and symlinks, registry items support optional properties `enhKey`, `withAttrs`, `canSpawn`, and `lifecycleKeys`:
1090
1335
 
1091
1336
  ```TypeScript
1092
- interface IEnhancementRegistryItem<T, TObj = Element> {
1337
+ interface EnhancementConfig<T, TObj = Element> {
1093
1338
  spawn: {
1094
1339
  new (obj?: TObj, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
1095
1340
  canSpawn?: (obj: TObj, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
@@ -1229,6 +1474,37 @@ console.log(element.enh.myEnh === instance); // true
1229
1474
  - **Shared instances**: Uses the same global instance map as `assignGingerly` and `enh.set`, ensuring only one instance per registry item
1230
1475
  - **Auto-registration**: Automatically adds registry items to the element's registry if not present
1231
1476
 
1477
+ **Lookup by enhKey (string or symbol):**
1478
+
1479
+ Instead of passing the full registry item object, you can pass a string or symbol matching the `enhKey` of a previously registered enhancement:
1480
+
1481
+ ```TypeScript
1482
+ // First, register the enhancement (e.g., via mount-observer or manually)
1483
+ registry.push({
1484
+ spawn: MyEnhancement,
1485
+ enhKey: 'myEnh'
1486
+ });
1487
+
1488
+ // Later, retrieve by enhKey string
1489
+ const instance = element.enh.get('myEnh');
1490
+
1491
+ // Or by symbol enhKey
1492
+ const enhSym = Symbol.for('myEnh');
1493
+ const instance2 = element.enh.get(enhSym);
1494
+ ```
1495
+
1496
+ If the enhKey is not found in the registry, an error is thrown: `"myEnh not in registry"`.
1497
+
1498
+ This also works with `enh.dispose()` and `enh.whenResolved()`:
1499
+
1500
+ ```TypeScript
1501
+ // Dispose by enhKey
1502
+ element.enh.dispose('myEnh');
1503
+
1504
+ // Wait for resolution by enhKey
1505
+ const resolved = await element.enh.whenResolved('myEnh');
1506
+ ```
1507
+
1232
1508
  <details>
1233
1509
  <summary>Example with shared instances</summary>
1234
1510
 
@@ -1779,7 +2055,7 @@ static canSpawn(obj: any, ctx?: SpawnContext<T>): boolean
1779
2055
  ```
1780
2056
 
1781
2057
  - `obj`: The target object being enhanced (element, plain object, etc.)
1782
- - `ctx`: Optional spawn context containing `{ config: IEnhancementRegistryItem<T> }`
2058
+ - `ctx`: Optional spawn context containing `{ config: EnhancementConfig<T> }`
1783
2059
  - Returns: `true` to allow spawning, `false` to block
1784
2060
 
1785
2061
  ### Use Cases