assign-gingerly 0.0.41 → 0.0.43

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
@@ -4656,6 +4656,9 @@ When the custom element's `connectedCallback` fires, the feature's `connectedCal
4656
4656
  | `disconnectedCallback` | Feature needs cleanup (remove listeners, abort fetches) |
4657
4657
  | `attributeChangedCallback` | Feature reacts to attribute changes (limited to element's `observedAttributes`) |
4658
4658
  | `adoptedCallback` | Feature reacts to document adoption |
4659
+ | `formDisabledCallback` | Feature reacts to disabled state changes (form-associated elements) |
4660
+ | `formResetCallback` | Feature reacts to form reset (form-associated elements) |
4661
+ | `formStateRestoreCallback` | Feature restores state after navigation/session restore (form-associated elements) |
4659
4662
 
4660
4663
  **Example: Feature that reads computed styles on connect**
4661
4664
 
@@ -4723,6 +4726,74 @@ customElements.assignFeatures(MyElement, {
4723
4726
 
4724
4727
  **Note on `attributeChangedCallback`:** The feature only receives callbacks for attributes listed in the element's `static observedAttributes`. Features cannot add to this list after `define()` is called.
4725
4728
 
4729
+ ### Class-level setup with `static onAssigned`
4730
+
4731
+ Some features need one-time class-level setup before any instances are created — for example, installing prototype getter/setters or pre-loading modules. The `static onAssigned` method on the spawn class is called by `assignFeatures` immediately after registration:
4732
+
4733
+ ```JavaScript
4734
+ class RoundaboutFeature {
4735
+ // Called once when assignFeatures processes this feature
4736
+ static async onAssigned(ctr, featureConfig) {
4737
+ // One-time class-level setup: install prototype getter/setters, pre-load modules
4738
+ await makeRoundaboutReady(ctr, featureConfig.customData);
4739
+ }
4740
+
4741
+ constructor(host, ctx, initVals) {
4742
+ // Instance-level setup (runs on first getter access)
4743
+ const [vm, propagator] = roundaboutSync({
4744
+ vm: host,
4745
+ ...ctx.injection.customData,
4746
+ });
4747
+ this._vm = vm;
4748
+ this._propagator = propagator;
4749
+ }
4750
+ }
4751
+ ```
4752
+
4753
+ **Usage:**
4754
+
4755
+ ```JavaScript
4756
+ // await is safe — returns undefined if no async onAssigned hooks exist
4757
+ await customElements.assignFeatures(MyElement, {
4758
+ roundabout: {
4759
+ spawn: RoundaboutFeature,
4760
+ customData: raConfig
4761
+ }
4762
+ });
4763
+
4764
+ // Now define — class is fully set up, connectedCallback will be synchronous
4765
+ customElements.define('my-element', MyElement);
4766
+ ```
4767
+
4768
+ **How it works:**
4769
+
4770
+ - `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
4771
+ - If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
4772
+ - If `onAssigned` returns a Promise, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
4773
+ - If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible — existing code that doesn't `await` still works).
4774
+ - Only applies to synchronous spawners (the class must be available at registration time). Async spawners can't define `onAssigned` since the class isn't loaded yet.
4775
+
4776
+ **`await` is always safe:**
4777
+
4778
+ ```JavaScript
4779
+ // These are equivalent for sync features (no onAssigned or sync onAssigned):
4780
+ customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4781
+ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4782
+ // Both work — await on undefined is a no-op
4783
+ ```
4784
+
4785
+ <details>
4786
+ <summary>Catalog of Published Custom Element Features</summary>
4787
+
4788
+ | Package | Description | Source |
4789
+ |---------|-------------|--------|
4790
+ | [truth-sourcer](https://www.npmjs.com/package/truth-sourcer) | Attribute/property binding and truth-sourcing for custom elements | [GitHub](https://github.com/bahrus/truth-sourcer) |
4791
+ | [be-reflective](https://www.npmjs.com/package/be-reflective) | CSS custom state reflection from computed styles | [GitHub](https://github.com/bahrus/be-reflective) |
4792
+ | [face-up](https://www.npmjs.com/package/face-up) | Form Associated Custom Element behavior via ElementInternals | [GitHub](https://github.com/bahrus/face-up) |
4793
+ | [time-ticker](https://www.npmjs.com/package/time-ticker) | Web component that fires events periodically (example of a feature-based component with no code in the class) | [GitHub](https://github.com/bahrus/time-ticker) |
4794
+
4795
+ </details>
4796
+
4726
4797
  ### Roadmap (future phases)
4727
4798
 
4728
4799
  - **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
package/assignFeatures.js CHANGED
@@ -305,7 +305,10 @@ const VALID_CALLBACKS = new Set([
305
305
  'connectedCallback',
306
306
  'disconnectedCallback',
307
307
  'attributeChangedCallback',
308
- 'adoptedCallback'
308
+ 'adoptedCallback',
309
+ 'formDisabledCallback',
310
+ 'formResetCallback',
311
+ 'formStateRestoreCallback'
309
312
  ]);
310
313
  /**
311
314
  * WeakMap tracking which callbacks have been patched on which constructors,
@@ -383,6 +386,7 @@ export function assignFeatures(ctr, features, featuresRegistry) {
383
386
  if (!supportedFeatures) {
384
387
  throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
385
388
  }
389
+ const onAssignedPromises = [];
386
390
  for (const key of Object.keys(features)) {
387
391
  // 1. Confirm the key is opted-in via supportedFeatures
388
392
  if (!(key in supportedFeatures)) {
@@ -411,8 +415,18 @@ export function assignFeatures(ctr, features, featuresRegistry) {
411
415
  if (allCallbacks.length > 0) {
412
416
  installCallbackForwarding(ctr, key, allCallbacks);
413
417
  }
418
+ // 7. Call static onAssigned if the spawn class defines it
419
+ const SpawnClass = featureConfig.spawn;
420
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
421
+ Object.hasOwn(SpawnClass, 'onAssigned') &&
422
+ typeof SpawnClass.onAssigned === 'function') {
423
+ const result = SpawnClass.onAssigned(ctr, featureConfig);
424
+ if (result && typeof result.then === 'function') {
425
+ onAssignedPromises.push(result);
426
+ }
427
+ }
414
428
  }
415
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
429
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
416
430
  const featuresConfig = ctr.featuresConfig;
417
431
  if (featuresConfig?.lifecycleKeys) {
418
432
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -420,6 +434,11 @@ export function assignFeatures(ctr, features, featuresRegistry) {
420
434
  installWhenFeatureReadyMethod(ctr, methodName);
421
435
  }
422
436
  }
437
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
438
+ if (onAssignedPromises.length > 0) {
439
+ return Promise.all(onAssignedPromises).then(() => { });
440
+ }
441
+ return undefined;
423
442
  }
424
443
  /**
425
444
  * Captures own-properties that shadow feature getters and stores them as initVals.
@@ -525,7 +544,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
525
544
  });
526
545
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
527
546
  value: function (ctr, features) {
528
- assignFeatures(ctr, features, this.featuresRegistry);
547
+ return assignFeatures(ctr, features, this.featuresRegistry);
529
548
  },
530
549
  writable: true,
531
550
  enumerable: false,
package/assignFeatures.ts CHANGED
@@ -515,7 +515,10 @@ const VALID_CALLBACKS = new Set([
515
515
  'connectedCallback',
516
516
  'disconnectedCallback',
517
517
  'attributeChangedCallback',
518
- 'adoptedCallback'
518
+ 'adoptedCallback',
519
+ 'formDisabledCallback',
520
+ 'formResetCallback',
521
+ 'formStateRestoreCallback'
519
522
  ]);
520
523
 
521
524
  /**
@@ -607,7 +610,7 @@ export function assignFeatures(
607
610
  ctr: Function,
608
611
  features: FeatureConfigsMap,
609
612
  featuresRegistry: FeaturesRegistry
610
- ): void {
613
+ ): Promise<void> | undefined {
611
614
  // Validate that the constructor has static supportedFeatures
612
615
  const supportedFeatures: SupportedFeaturesMap | undefined = (ctr as any).supportedFeatures;
613
616
 
@@ -617,6 +620,8 @@ export function assignFeatures(
617
620
  );
618
621
  }
619
622
 
623
+ const onAssignedPromises: Promise<void>[] = [];
624
+
620
625
  for (const key of Object.keys(features)) {
621
626
  // 1. Confirm the key is opted-in via supportedFeatures
622
627
  if (!(key in supportedFeatures)) {
@@ -656,9 +661,20 @@ export function assignFeatures(
656
661
  if (allCallbacks.length > 0) {
657
662
  installCallbackForwarding(ctr, key, allCallbacks);
658
663
  }
664
+
665
+ // 7. Call static onAssigned if the spawn class defines it
666
+ const SpawnClass = featureConfig.spawn;
667
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
668
+ Object.hasOwn(SpawnClass as any, 'onAssigned') &&
669
+ typeof (SpawnClass as any).onAssigned === 'function') {
670
+ const result = (SpawnClass as any).onAssigned(ctr, featureConfig);
671
+ if (result && typeof result.then === 'function') {
672
+ onAssignedPromises.push(result);
673
+ }
674
+ }
659
675
  }
660
676
 
661
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
677
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
662
678
  const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
663
679
  if (featuresConfig?.lifecycleKeys) {
664
680
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -666,6 +682,12 @@ export function assignFeatures(
666
682
  installWhenFeatureReadyMethod(ctr, methodName);
667
683
  }
668
684
  }
685
+
686
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
687
+ if (onAssignedPromises.length > 0) {
688
+ return Promise.all(onAssignedPromises).then(() => {});
689
+ }
690
+ return undefined;
669
691
  }
670
692
 
671
693
  /**
@@ -772,7 +794,7 @@ export class PropertyBag {
772
794
  declare global {
773
795
  interface CustomElementRegistry {
774
796
  featuresRegistry: FeaturesRegistry;
775
- assignFeatures(ctr: Function, features: FeatureConfigsMap): void;
797
+ assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
776
798
  }
777
799
  }
778
800
 
@@ -793,8 +815,8 @@ if (typeof CustomElementRegistry !== 'undefined') {
793
815
  });
794
816
 
795
817
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
796
- value: function (ctr: Function, features: FeatureConfigsMap): void {
797
- assignFeatures(ctr, features, this.featuresRegistry);
818
+ value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
819
+ return assignFeatures(ctr, features, this.featuresRegistry);
798
820
  },
799
821
  writable: true,
800
822
  enumerable: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
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": {
@@ -179,13 +179,18 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
179
179
  // * Defaults to true (initial read only)
180
180
  // */
181
181
  // initialOnly?: boolean;
182
+
183
+ /**
184
+ * Should make sure it is added to static observedAttribrutes
185
+ */
186
+ sourceOfTruth?: boolean;
182
187
  }
183
188
 
184
189
  export type AttrPatterns<T = any> = {
185
190
  /**
186
191
  * Base prefix for attribute names
187
192
  */
188
- base: string;
193
+ base?: string;
189
194
 
190
195
  /**
191
196
  * Configuration for the base pattern
@@ -241,18 +246,6 @@ export interface IAssignGingerlyOptions {
241
246
  * When the signal is aborted, all event listeners are automatically removed
242
247
  */
243
248
  signal?: AbortSignal;
244
-
245
- /**
246
- * List of property names that should be treated as async methods.
247
- * Works together with withMethods — async methods are awaited before
248
- * continuing the chain.
249
- *
250
- * The path evaluation for keys containing async methods is fire-and-forget:
251
- * assignGingerly remains synchronous and returns immediately.
252
- *
253
- * NOTE: Interaction with @each and @eachTime is not yet implemented.
254
- */
255
- withAsyncMethods?: string[] | Set<string>;
256
249
  }
257
250
 
258
251
  /**
@@ -333,130 +326,3 @@ export interface ElementEnhancement{
333
326
  dispose(registryItem: EnhancementConfig | string | symbol): void;
334
327
  whenResolved(registryItem: EnhancementConfig | string | symbol, mountCtx?: any): Promise<any>;
335
328
  }
336
-
337
- /**
338
- * Context passed to feature spawn constructors
339
- */
340
- export interface FeatureSpawnContext {
341
- /** The feature key (e.g., 'photoTaker') */
342
- key: string;
343
- /** The SupportedFeatureConfig from static supportedFeatures */
344
- optIn: SupportedFeatureConfig;
345
- /** The FeatureConfig from assignFeatures */
346
- injection: FeatureConfig;
347
- /** The features registry reference */
348
- featuresRegistry: FeaturesRegistry;
349
- /** Shared context from the host element (via getSharedContext callback) */
350
- shared?: any;
351
- }
352
-
353
- /**
354
- * Configuration for a supported feature slot declared via static supportedFeatures
355
- */
356
- export interface SupportedFeatureConfig {
357
- /**
358
- * Optional fallback class (or async spawner) to use if no implementation is injected.
359
- */
360
- fallbackSpawn?:
361
- | { new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }
362
- | (() => Promise<{ new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }>);
363
-
364
- /**
365
- * Optional runtime shape validation for the spawned instance.
366
- * Return true if the instance is valid, false to throw.
367
- */
368
- validateShape?: (spawnedInstance: any) => boolean;
369
-
370
- /**
371
- * Optional callback to provide shared context (e.g., ElementInternals, private state)
372
- * to the feature at construction time.
373
- *
374
- * Defined in the class body, this callback has access to #private fields.
375
- * The returned object is passed to the feature constructor as `ctx.shared`.
376
- *
377
- * @param instance - The host element instance
378
- * @returns An object containing shared data for the feature
379
- */
380
- getSharedContext?: (instance: any) => any;
381
- }
382
-
383
- /**
384
- * Class-level configuration for the features system.
385
- * Declared as `static featuresConfig` on the class.
386
- */
387
- export interface FeaturesClassConfig {
388
- /**
389
- * Lifecycle method configuration.
390
- *
391
- * If set to `true`, installs a method named 'whenFeatureReady' on the prototype.
392
- * If set to an object, allows customizing the method name.
393
- *
394
- * The installed method accepts a feature key and returns a Promise that resolves
395
- * with the feature instance once it's ready (useful for async spawners).
396
- * For synchronous spawners, the Promise resolves immediately.
397
- *
398
- * Suggested default name: 'whenFeatureReady'
399
- */
400
- lifecycleKeys?: true | {
401
- /** Method name for awaiting feature readiness. Defaults to 'whenFeatureReady'. */
402
- whenFeatureReady?: string;
403
- };
404
- }
405
-
406
- /**
407
- * Configuration for a feature passed to assignFeatures.
408
- * The feature equivalent of EnhancementConfig.
409
- */
410
- export interface FeatureConfig {
411
- /**
412
- * The class to instantiate for this feature, or an async function that
413
- * resolves to such a class (for lazy-loading).
414
- *
415
- * Synchronous: Constructor receives the host element as its first argument,
416
- * a FeatureSpawnContext as second, and optional initVals as third.
417
- *
418
- * Asynchronous: A function (arrow or async) that returns a Promise resolving
419
- * to a constructor. The getter returns a placeholder object immediately and
420
- * instantiates the real class once the Promise resolves.
421
- */
422
- spawn?:
423
- | { new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }
424
- | (() => Promise<{ new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }>);
425
-
426
- /**
427
- * Attribute patterns for parsing element attributes into initVals.
428
- * Attributes are the "base layer" — programmatic values override them.
429
- * Always unprefixed for features (no enh- prefix).
430
- */
431
- withAttrs?: AttrPatterns<any>;
432
-
433
- /**
434
- * Reserved field for custom configuration data.
435
- * Not interpreted by the library — available to the feature class
436
- * via ctx.injection.customData in the constructor.
437
- */
438
- customData?: any;
439
- }
440
-
441
- export type SupportedFeaturesMap = Record<string, SupportedFeatureConfig>;
442
- export type FeatureConfigsMap = Record<string, FeatureConfig>;
443
-
444
- /**
445
- * Registry for feature configs, keyed by constructor
446
- */
447
- export declare class FeaturesRegistry {
448
- has(ctr: Function): boolean;
449
- get(ctr: Function): Map<string, FeatureConfig> | undefined;
450
- set(ctr: Function, key: string, config: FeatureConfig): void;
451
- hasKey(ctr: Function, key: string): boolean;
452
- }
453
-
454
- /**
455
- * Core assignFeatures function.
456
- * Validates inputs, registers feature configs, and installs lazy getters on the class prototype.
457
- */
458
- export declare function assignFeatures(
459
- ctr: Function,
460
- features: FeatureConfigsMap,
461
- featuresRegistry: FeaturesRegistry
462
- ): void;