assign-gingerly 0.0.96 → 0.0.97

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
@@ -1594,7 +1594,83 @@ When you access `element.enh.set.enhKey.property`, the proxy:
1594
1594
  - If a non-matching object already exists at `element.enh[enhKey]`, it's passed as `initVals`
1595
1595
  - Stores the spawned instance at `element.enh[enhKey]`
1596
1596
  3. **Reuses existing instances**: If the enhancement already exists and is the correct type, it reuses it
1597
- 4. **Falls back to plain objects**: If no registry item is found, creates a plain object at `element.enh[enhKey]`
1597
+ 4. **Falls back to plain objects, and waits for registration**: If no registry item is found yet, creates a plain object at `element.enh[enhKey]` and calls `registry.whenDefined(enhKey)` to spawn the real instance automatically once the registry item eventually arrives — see [Setting Properties Before the Registry Item Exists](#setting-properties-before-the-registry-item-exists) below
1598
+
1599
+ </details>
1600
+
1601
+ ### Setting Properties Before the Registry Item Exists
1602
+
1603
+ Code doesn't always run in the order you'd like — a module might set
1604
+ properties on an enhancement before whatever registers that enhancement's
1605
+ config has had a chance to run. `enh.set` handles this automatically, the
1606
+ same way `element.enh.set.enhKey.property = value` does when the registry
1607
+ item is already there:
1608
+
1609
+ ```TypeScript
1610
+ import 'assign-gingerly/object-extension.js';
1611
+
1612
+ const myElement = document.createElement('div');
1613
+
1614
+ // Nothing has called `registry.push(...)` for 'myEnh' yet.
1615
+ myElement.enh.set.myEnh.myProp = 'hello'; // plain placeholder, collected as pending initVals
1616
+ myElement.enh.myEnh.anotherProp = 'world'; // direct property access works too, same placeholder
1617
+
1618
+ console.log(myElement.enh.myEnh instanceof MyEnhancement); // false -- still a plain object
1619
+
1620
+ // ...later, whenever the registry item shows up...
1621
+ const registry = myElement.customElementRegistry.enhancementRegistry;
1622
+ registry.push({ spawn: MyEnhancement, enhKey: 'myEnh' });
1623
+
1624
+ // The spawn happens automatically once `whenDefined` resolves -- no need to
1625
+ // touch `myElement.enh.myEnh` again yourself.
1626
+ await registry.whenDefined('myEnh');
1627
+ console.log(myElement.enh.myEnh instanceof MyEnhancement); // true
1628
+ console.log(myElement.enh.myEnh.myProp); // 'hello' -- both properties
1629
+ console.log(myElement.enh.myEnh.anotherProp); // 'world' -- survived as initVals
1630
+ ```
1631
+
1632
+ <details>
1633
+ <summary>How it works</summary>
1634
+
1635
+ 1. The first `enh.set.enhKey.property = value` assignment for an
1636
+ unregistered `enhKey` creates a plain object at `element.enh[enhKey]` (as
1637
+ always) and, in addition, calls
1638
+ `registry.whenDefined(enhKey).then(() => element.enh.get(enhKey))`.
1639
+ 2. Because that call only happens inside the `if (self[prop] === undefined)`
1640
+ branch, it fires exactly once per element/`enhKey` pair — further property
1641
+ assignments before registration just add to the same placeholder object,
1642
+ they don't queue additional spawn attempts.
1643
+ 3. `registry.whenDefined(enhKey)` resolves once a matching `enhKey` is
1644
+ `push`ed into the registry (see below).
1645
+ 4. Once it resolves, `enh.get(enhKey)` runs — the same method used everywhere
1646
+ else in this library. It finds the plain placeholder already sitting at
1647
+ `element.enh[enhKey]`, treats it as `initVals` (this is the same
1648
+ "non-matching object passed as `initVals`" behavior described in step 2 of
1649
+ [How It Works](#basic-usage) above — nothing new here), constructs the real
1650
+ instance, and overwrites the placeholder with it.
1651
+
1652
+ **`EnhancementRegistry.whenDefined(enhKey)`** — mirrors
1653
+ [`customElements.whenDefined(name)`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined):
1654
+
1655
+ - If `enhKey` is already registered, resolves once any of its pending
1656
+ [Custom Element Features](#custom-element-features) setup has settled (see
1657
+ `features` on `EnhancementConfig`).
1658
+ - If `enhKey` isn't registered yet, first waits for a future `registry.push(...)`
1659
+ call whose config (a single item, or an array) includes a matching `enhKey`,
1660
+ then does the same features-setup wait.
1661
+ - **If `enhKey` is never registered, this promise never resolves** — same
1662
+ tradeoff `customElements.whenDefined` makes. Don't `await` it unconditionally
1663
+ in code paths where registration isn't guaranteed to eventually happen.
1664
+
1665
+ **Limitations while waiting:**
1666
+ - The placeholder is a plain object — no reactivity, no defaults, no
1667
+ validation. Reading a property other than the ones you've explicitly set
1668
+ (e.g. checking `resolved`) just returns `undefined`, since nothing has
1669
+ spawned yet.
1670
+ - `mountCtx` still isn't available through this path (same restriction as
1671
+ `enh.set` generally — see the note in
1672
+ [Passing Custom Context](#constructor-signature) above); the eventual
1673
+ `enh.get(enhKey)` call is made with no `mountCtx`.
1598
1674
 
1599
1675
  </details>
1600
1676
 
package/assignGingerly.js CHANGED
@@ -63,10 +63,27 @@ export class EnhancementRegistry extends EventTarget {
63
63
  }
64
64
  }
65
65
  /**
66
- * Wait for all pending setups for a given enhancement key to complete.
66
+ * Resolves once `enhKey` is defined -- i.e. registered via `push` -- and
67
+ * ready (any pending `features` setup for it has settled). Mirrors
68
+ * `customElements.whenDefined(name)`: if the key isn't registered yet, waits
69
+ * for a future `push` that registers it; if it's never registered, this
70
+ * never resolves (same tradeoff as the platform method).
67
71
  * @param enhKey - Enhancement key to wait for
68
72
  */
69
73
  async whenDefined(enhKey) {
74
+ if (!this.findByEnhKey(enhKey)) {
75
+ await new Promise((resolve) => {
76
+ const handler = (event) => {
77
+ const config = event.config;
78
+ const items = Array.isArray(config) ? config : [config];
79
+ if (items.some(item => item.enhKey === enhKey)) {
80
+ this.removeEventListener(EnhancementRegisteredEvent.eventName, handler);
81
+ resolve();
82
+ }
83
+ };
84
+ this.addEventListener(EnhancementRegisteredEvent.eventName, handler);
85
+ });
86
+ }
70
87
  const pending = this.#pendingSetups.get(enhKey);
71
88
  if (pending && pending.length > 0) {
72
89
  await Promise.all(pending);
package/assignGingerly.ts CHANGED
@@ -106,10 +106,27 @@ export class EnhancementRegistry extends EventTarget {
106
106
  }
107
107
 
108
108
  /**
109
- * Wait for all pending setups for a given enhancement key to complete.
109
+ * Resolves once `enhKey` is defined -- i.e. registered via `push` -- and
110
+ * ready (any pending `features` setup for it has settled). Mirrors
111
+ * `customElements.whenDefined(name)`: if the key isn't registered yet, waits
112
+ * for a future `push` that registers it; if it's never registered, this
113
+ * never resolves (same tradeoff as the platform method).
110
114
  * @param enhKey - Enhancement key to wait for
111
115
  */
112
116
  async whenDefined(enhKey: EnhKey): Promise<void> {
117
+ if (!this.findByEnhKey(enhKey)) {
118
+ await new Promise<void>((resolve) => {
119
+ const handler = (event: Event) => {
120
+ const config = (event as EnhancementRegisteredEvent).config;
121
+ const items = Array.isArray(config) ? config : [config];
122
+ if (items.some(item => item.enhKey === enhKey)) {
123
+ this.removeEventListener(EnhancementRegisteredEvent.eventName, handler);
124
+ resolve();
125
+ }
126
+ };
127
+ this.addEventListener(EnhancementRegisteredEvent.eventName, handler);
128
+ });
129
+ }
113
130
  const pending = this.#pendingSetups.get(enhKey);
114
131
  if (pending && pending.length > 0) {
115
132
  await Promise.all(pending);
@@ -107,7 +107,7 @@ Update the package.json to use the modern architecture's dependencies and build
107
107
  "nested-regex-groups": "*"
108
108
  }
109
109
  ```
110
- - Drop the legacy dependencies (`be-enhanced`, `trans-render`, etc.). Keep a legacy dependency **only** if a converted action still imports from it and the modern stack has no replacement (e.g. `trans-render/XV/set.js` for the Uniform Storage Path protocol) — note any such carry-over in your conversion notes.
110
+ - Drop the legacy dependencies (`be-enhanced`, `trans-render`, etc.). There is a modern replacement for most legacy utilities — e.g. Uniform Storage Path writes move from `trans-render/XV/set.js` to [`fifteenth`](https://github.com/bahrus/fifteenth) (`fifteenth/set.js`, see Step 9). Keep a legacy dependency **only** if a converted action still imports from it and no replacement exists — and note any such carry-over in your conversion notes.
111
111
  - `mount-observer` is listed as a direct dependency so it lands at the root `node_modules/` level, accessible via the import map. `nested-regex-groups` is optional but recommended if your enhancement needs custom attribute parsing (Step 7a).
112
112
  - The exact version strings do not matter here — the next step replaces them all with the latest published versions.
113
113
 
@@ -969,6 +969,14 @@ export interface Actions{
969
969
  7. **Update utility imports**:
970
970
  - Replace `trans-render/lib/findAdjacentElement.js` with `be-hive/findAdjacentElement.js`
971
971
  - The be-hive package provides common utilities that were previously in trans-render
972
+ - **Uniform Storage Path (USL) writes** — if a legacy action wrote values to `indexedDB://…`, `localStorage://…`, etc. via `trans-render/XV/set.js`, use [`fifteenth`](https://github.com/bahrus/fifteenth) instead:
973
+ ```javascript
974
+ const { set } = await import('fifteenth/set.js');
975
+ await set('indexedDB://myDB/myStore/myKey', value); // same (usl, val, ctx?) signature as XV/set
976
+ ```
977
+ - Add `"fifteenth"` to `dependencies` and a `"fifteenth/": "/node_modules/fifteenth/"` entry to the import map.
978
+ - `fifteenth` imports `parseProtocolRef` from `assign-gingerly@0.0.96`+. `roundabout-lib` / `mount-observer` may still pin an older `assign-gingerly` exactly, and the flat import map can only serve one copy — so also add `"assign-gingerly": "<the version fifteenth needs>"` to `dependencies` to hoist a single root copy. Verify the be-hive/mount-observer/roundabout read path still works against that version.
979
+ - **Behavior change to note in your conversion notes:** `fifteenth`'s `set` broadcasts `window.postMessage([usp])` (or `[usp, usl]` when the USL has a `?.` accessor chain) — a plain `Array` of path strings, *not* the `Set` (protocol + store-root + full-path) that `trans-render/XV` posted. Update any `message` listeners accordingly.
972
980
 
973
981
  **CRITICAL - Avoid Compact/Action Conflicts:**
974
982
 
@@ -0,0 +1,46 @@
1
+ # Clarify Programmatic Way to Add Enhancements
2
+
3
+ When we define an enhancement, it is easiest to demonstrate what it does and how it works by providing simple HTML examples with the attributes used to trigger the enhancement.
4
+
5
+ These examples are skewed towards an important use case -- progressive enhancement of server rendered content.
6
+
7
+ However, currently, only a small fraction of web development centers around that paradigm. Rather, it tends to revolve around client-side rendering, using a framework.
8
+
9
+ In such a context, putting so much emphasis on the attribute way of hooking things up is problematic.
10
+
11
+ 1. Such frameworks are clumsy when it comes to setting attributes.
12
+ 2. The amount of object stringifying and parsing is inefficient.
13
+
14
+ So we should make an effort to showcase how to integrate with these enhancements efficiently and ergonomically in such settings. Basically programmatically without the use of attributes.
15
+
16
+ I think we should showcase two ways to do so.
17
+
18
+ So for example, currently the README.md for be-persistent shows the example:
19
+
20
+ ```html
21
+ <input be-persistent="of value@input via sessionStorage://{autoGenId}.">
22
+ ```
23
+
24
+ We should also show:
25
+
26
+ ## Imperative Enhancement Attachment:
27
+
28
+ ```JS
29
+ import {emc} from 'be-persistent/emc.json' with {type: 'json'};
30
+ import {BePersistent} from 'be-persistent/be-persistent.js';
31
+ emc.spawn = BePersistent;
32
+ const persistenceEnhancement = oInput.enh.get(emc);
33
+ persistenceEnhancement.store =
34
+ {
35
+ localProp: 'value', //default
36
+ localEvent: 'input', //default
37
+ usl: 'sessionStorage://{autoGenId}'
38
+ };
39
+ ```
40
+
41
+ ## Declarative Enhancement Attachment:
42
+
43
+ ```JS
44
+ import 'be-persistent/.js';
45
+
46
+ ```
@@ -72,6 +72,8 @@ export interface EnhancementConfig<T = any, Obj = Element> extends EnhancementCo
72
72
  * Calls assignFeatures(spawn, features) automatically on registration.
73
73
  */
74
74
  features?: FeatureConfigsMap;
75
+
76
+ customData?: any;
75
77
 
76
78
  }
77
79
 
@@ -406,7 +408,7 @@ export interface AssignFromOptions {
406
408
  from: any;
407
409
 
408
410
  /** Protocol handlers (sync or async) */
409
- protocols?: Record<string, (key: string) => any | Promise<any>>;
411
+ protocols?: ProtocolHandlers;
410
412
 
411
413
  /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
412
414
  withMethods?: string[] | Set<string>;
@@ -480,6 +482,37 @@ export interface AssignFromOptions {
480
482
  [key: string]: any;
481
483
  }
482
484
 
485
+ /**
486
+ * A protocol handler resolves the key portion of a protocol-prefixed value
487
+ * (the text between `://` and the first `?.`) to a value. May be sync or async.
488
+ */
489
+ export type ProtocolHandler = (key: string) => any | Promise<any>;
490
+
491
+ /**
492
+ * A synchronous-only protocol handler, as accepted by getValues / getValue /
493
+ * assignFrom. For handlers that may return a Promise, use ProtocolHandler.
494
+ */
495
+ export type SyncProtocolHandler = (key: string) => any;
496
+
497
+ /** Map of protocol name (the text before `://`) to a handler (sync or async). */
498
+ export type ProtocolHandlers = Record<string, ProtocolHandler>;
499
+
500
+ /** Map of protocol name to a synchronous handler. */
501
+ export type SyncProtocolHandlers = Record<string, SyncProtocolHandler>;
502
+
503
+ /**
504
+ * The outer grammar of a protocol-prefixed value string,
505
+ * `‹protocol›://‹key›?.‹path›`, as produced by `parseProtocolRef`.
506
+ */
507
+ export interface ParsedProtocolRef {
508
+ /** Text before `://`; `''` when the value contains no `://`. */
509
+ protocol: string;
510
+ /** Text between `://` and the first `?.` (or the end of the string). */
511
+ key: string;
512
+ /** Text from the first `?.` onward (always starts with `?.`), or `null`. */
513
+ path: string | null;
514
+ }
515
+
483
516
  /**
484
517
  * Options for synchronous value resolution (getValues / getValue).
485
518
  * Extends IAssignGingerlyOptions with synchronous protocol handlers.
@@ -507,7 +540,7 @@ export interface GetValuesOptions extends IAssignGingerlyOptions {
507
540
  * localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
508
541
  * }
509
542
  */
510
- protocols?: Record<string, (key: string) => any>;
543
+ protocols?: SyncProtocolHandlers;
511
544
  }
512
545
 
513
546
  /**
@@ -530,7 +563,7 @@ export interface ResolveValuesOptions extends IAssignGingerlyOptions {
530
563
  * Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
531
564
  * Each handler receives the key portion and returns the resolved value (sync or async).
532
565
  */
533
- protocols?: Record<string, (key: string) => any | Promise<any>>;
566
+ protocols?: ProtocolHandlers;
534
567
  }
535
568
 
536
569
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Uniform Storage Path string (e.g. "indexedDB://myDB/myFiles/{file.name}").
3
- * Inlined here (was imported from trans-render/XV/types) to keep this file standalone.
3
+ * Consumed by `fifteenth`'s `set`. Kept as a local alias so this file stays standalone.
4
4
  */
5
5
  export type USL = string;
6
6
 
@@ -0,0 +1,81 @@
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
+ import { StatementsResult } from "../nested-regex-groups/types";
3
+
4
+ /**
5
+ * The programmatic shape of `store`: a bare USL string (shorthand for
6
+ * `{usl}`, i.e. `localProp`/`localEvent` take their defaults), a single
7
+ * {@link PersistenceRule}, or an array of them (one binder per entry).
8
+ * This is the property JS callers set directly — e.g.
9
+ * `persistenceEnhancement.store = 'sessionStorage://{autoGenId}'` — without
10
+ * ever going through attribute parsing.
11
+ */
12
+ export type PersistenceRuleConfig = string | PersistenceRule | PersistenceRule[];
13
+
14
+ export interface EndUserProps {
15
+ /**
16
+ * What to persist and where. Accepts a USL string, a single
17
+ * {@link PersistenceRule}, or an array of rules — see
18
+ * {@link PersistenceRuleConfig}. This is the property `hydrate` reads and
19
+ * the one programmatic (attribute-free) callers should assign.
20
+ */
21
+ store?: PersistenceRuleConfig;
22
+ /**
23
+ * Opt-in flag set by the boolean `be-persistent-nudge` / `💾-nudge`
24
+ * attribute. When present, `hydrate` waits for every rule to finish its
25
+ * initial storage reconciliation and then calls `assign-gingerly`'s
26
+ * `nudge` on the enhanced element — decrementing its `disabled` counter so
27
+ * an element that was disabled only to block edits before its persisted
28
+ * value loaded becomes interactive.
29
+ */
30
+ nudge?: boolean;
31
+ }
32
+
33
+ export interface AllProps extends EndUserProps {
34
+ enhancedElement: Element & ElementEnhancementGateway;
35
+ resolved?: boolean;
36
+ /**
37
+ * Flipped to `true` at the end of `init`, once `roundabout` has finished
38
+ * assigning every attribute-derived prop. `hydrate` is gated on it so it
39
+ * never runs against a half-populated view model.
40
+ */
41
+ initialized?: boolean;
42
+ /**
43
+ * Raw parse result of the base attribute (`be-persistent` / `💾`), via the
44
+ * `parse-grouped-capture-statements` built-in parser. Exists **only** to be
45
+ * transferred into `store` by `onPersistenceRulesChange` — nothing else
46
+ * reads it. Programmatic callers should assign `store` directly instead of
47
+ * this property.
48
+ */
49
+ persistenceRules?: StatementsResult<PersistenceRule>;
50
+ }
51
+
52
+ export type AP = AllProps;
53
+
54
+ export type PAP = Partial<AP>;
55
+
56
+ export type ProPAP = Promise<PAP>;
57
+
58
+ export interface Actions {
59
+ init(self: AP, enhancedElement: Element & ElementEnhancementGateway, ctx: SpawnContext, initVals: PAP): Promise<void>;
60
+ /**
61
+ * Compact-invoked (`when_persistenceRules_changes_call_onPersistenceRulesChange`),
62
+ * not an action — normalizes `persistenceRules.statements` into `store`.
63
+ */
64
+ onPersistenceRulesChange(self: AP): PAP;
65
+ hydrate(self: AP): ProPAP;
66
+ }
67
+
68
+ export interface PersistenceRule {
69
+ /** Element property to read/write (e.g. `value`, `checked`, `innerHTML`). Defaults to `value`. */
70
+ localProp?: string;
71
+ /** DOM event that triggers a save. Defaults to `input`. */
72
+ localEvent?: string;
73
+ /**
74
+ * Uniform Storage Locator describing where the value lives, e.g.
75
+ * `sessionStorage://{autoGenId}`, `indexedDB://myDB/myStore/{autoGenId}`,
76
+ * `cookie://{autoGenId}`, `locationHash://{autoGenId}`.
77
+ * `{autoGenId}` is expanded at runtime to a location-independent DOM path.
78
+ * Defaults to `sessionStorage://{autoGenId}`.
79
+ */
80
+ usl?: string;
81
+ }
@@ -0,0 +1,59 @@
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
+
3
+ export interface EndUserProps {
4
+ /**
5
+ * Where to fetch the replacement content from — either a literal URL
6
+ * (typically a `gist.githubusercontent.com/.../raw/.../<file>` URL) or a
7
+ * `fifteenth` `gist://` USL (`gist://<owner>/<id>/raw[/<sha>]/<file>`,
8
+ * or an alias/`=<id>` form once `configureGist()` has registered the
9
+ * `gist` protocol).
10
+ */
11
+ url: string;
12
+ /** The `<?marker name="…">` this template's content replaces. */
13
+ markerName: string;
14
+ /**
15
+ * CSS selector naming the parent(s) of the marker(s), so the search
16
+ * doesn't have to `TreeWalker` the whole subtree. Optional.
17
+ */
18
+ forHint: string | undefined;
19
+ /**
20
+ * Show a link to edit the gist on GitHub right after the template.
21
+ * Also triggered page-wide by `?gist-in-show-edit-link=true` in the URL.
22
+ */
23
+ showEditLink: boolean;
24
+ /**
25
+ * Custom Sanitizer configuration (JSON object, e.g.
26
+ * `{"elements":["option","optgroup"]}`), same shape as pipe-in's
27
+ * `[base]-sanitizer`. The platform's *default* sanitizer strips elements
28
+ * like `<option>` entirely — this is the escape hatch for patching them.
29
+ * Gated the same way pipe-in gates it: only honored when `url` is a
30
+ * same-origin path, an import-map-resolved bare specifier, or a
31
+ * `gist://` USL (whose real destination is always the fixed
32
+ * `gist.githubusercontent.com` host, not attacker-steerable).
33
+ */
34
+ sanitizer: object | undefined;
35
+ /**
36
+ * `'setHTML'` (default, sanitized) or `'setHTMLUnsafe'` (no sanitizing at
37
+ * all) — named after, and selecting between, the two real underlying
38
+ * methods, the same way pipe-in's `[base]-method` selects one of its own
39
+ * `streamHTML`/`streamHTMLUnsafe`/etc. `'setHTMLUnsafe'` is gated the same
40
+ * as a custom `sanitizer` (see above). Never runs embedded `<script>`s
41
+ * either way — `setHTML`/`setHTMLUnsafe` parse, they don't execute
42
+ * (verified; same as plain `innerHTML`).
43
+ */
44
+ method: 'setHTML' | 'setHTMLUnsafe';
45
+ }
46
+
47
+ export interface AllProps extends EndUserProps {
48
+ enhancedElement: Element & ElementEnhancementGateway;
49
+ resolved: boolean;
50
+ }
51
+
52
+ export type AP = AllProps;
53
+ export type PAP = Partial<AP>;
54
+ export type ProPAP = Promise<PAP>;
55
+
56
+ export interface Actions {
57
+ init(self: AP, enhancedElement: Element, ctx: SpawnContext, initVals: PAP): Promise<void>;
58
+ hydrate(self: AP): ProPAP;
59
+ }
@@ -317,6 +317,16 @@ class ElementEnhancementContainer {
317
317
  // No registry item found - create plain object if needed
318
318
  if (self[prop] === undefined) {
319
319
  self[prop] = {};
320
+ // Declarative-out-of-sequence: `prop`'s enhancement config hasn't
321
+ // been registered yet. Once it is, hand the plain object we just
322
+ // created off to `get()` as pending initVals and let it spawn
323
+ // normally -- `get()` already treats a non-instance value sitting
324
+ // in this slot as initVals to merge in (see the `existingInitVals`
325
+ // check above). If `prop` is never registered, this never
326
+ // resolves (same tradeoff as `customElements.whenDefined`).
327
+ const laterRegistry = registry
328
+ ?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
329
+ laterRegistry?.whenDefined(prop).then(() => self.get(prop));
320
330
  }
321
331
  return self[prop];
322
332
  }
@@ -443,8 +443,19 @@ class ElementEnhancementContainer {
443
443
  // No registry item found - create plain object if needed
444
444
  if (self[prop] === undefined) {
445
445
  self[prop] = {};
446
+
447
+ // Declarative-out-of-sequence: `prop`'s enhancement config hasn't
448
+ // been registered yet. Once it is, hand the plain object we just
449
+ // created off to `get()` as pending initVals and let it spawn
450
+ // normally -- `get()` already treats a non-instance value sitting
451
+ // in this slot as initVals to merge in (see the `existingInitVals`
452
+ // check above). If `prop` is never registered, this never
453
+ // resolves (same tradeoff as `customElements.whenDefined`).
454
+ const laterRegistry = registry
455
+ ?? (typeof customElements !== 'undefined' ? (customElements as any).enhancementRegistry : undefined);
456
+ laterRegistry?.whenDefined(prop).then(() => self.get(prop));
446
457
  }
447
-
458
+
448
459
  return self[prop];
449
460
  }
450
461
  }) as any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.96",
3
+ "version": "0.0.97",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -228,7 +228,7 @@
228
228
  "chrome": "npx playwright cr http://localhost:8000"
229
229
  },
230
230
  "devDependencies": {
231
- "@playwright/test": "1.62.1",
232
- "spa-ssi": "0.0.27"
231
+ "@playwright/test": "1.63.0",
232
+ "spa-ssi": "0.0.28"
233
233
  }
234
234
  }
@@ -72,6 +72,8 @@ export interface EnhancementConfig<T = any, Obj = Element> extends EnhancementCo
72
72
  * Calls assignFeatures(spawn, features) automatically on registration.
73
73
  */
74
74
  features?: FeatureConfigsMap;
75
+
76
+ customData?: any;
75
77
 
76
78
  }
77
79