assign-gingerly 0.0.40 → 0.0.42

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
@@ -4617,13 +4617,24 @@ customElements.assignFeatures(MyBehaviors, { anything: { spawn: AnythingImpl } }
4617
4617
 
4618
4618
  ### Lifecycle callback forwarding with `callbackForwarding`
4619
4619
 
4620
- Features can receive custom element lifecycle callbacks by declaring `callbackForwarding` in their config:
4620
+ Features can receive custom element lifecycle callbacks by declaring `callbackForwarding` in their config. This can be specified by the feature author (in `static supportedFeatures`) and/or by the consumer (in `assignFeatures`). Both are merged — the author declares what the feature intrinsically needs, the consumer can add more:
4621
4621
 
4622
4622
  ```JavaScript
4623
+ // Author declares what the feature needs
4624
+ class MyElement extends HTMLElement {
4625
+ static supportedFeatures = {
4626
+ reflector: {
4627
+ fallbackSpawn: Reflector,
4628
+ callbackForwarding: ['connectedCallback', 'disconnectedCallback']
4629
+ }
4630
+ }
4631
+ }
4632
+
4633
+ // Consumer can add more (but not remove author's)
4623
4634
  customElements.assignFeatures(MyElement, {
4624
4635
  reflector: {
4625
4636
  spawn: Reflector,
4626
- callbackForwarding: ['connectedCallback', 'disconnectedCallback']
4637
+ callbackForwarding: ['adoptedCallback'] // merged with author's
4627
4638
  }
4628
4639
  });
4629
4640
  ```
@@ -4712,6 +4723,62 @@ customElements.assignFeatures(MyElement, {
4712
4723
 
4713
4724
  **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.
4714
4725
 
4726
+ ### Class-level setup with `static onAssigned`
4727
+
4728
+ 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:
4729
+
4730
+ ```JavaScript
4731
+ class RoundaboutFeature {
4732
+ // Called once when assignFeatures processes this feature
4733
+ static async onAssigned(ctr, featureConfig) {
4734
+ // One-time class-level setup: install prototype getter/setters, pre-load modules
4735
+ await makeRoundaboutReady(ctr, featureConfig.customData);
4736
+ }
4737
+
4738
+ constructor(host, ctx, initVals) {
4739
+ // Instance-level setup (runs on first getter access)
4740
+ const [vm, propagator] = roundaboutSync({
4741
+ vm: host,
4742
+ ...ctx.injection.customData,
4743
+ });
4744
+ this._vm = vm;
4745
+ this._propagator = propagator;
4746
+ }
4747
+ }
4748
+ ```
4749
+
4750
+ **Usage:**
4751
+
4752
+ ```JavaScript
4753
+ // await is safe — returns undefined if no async onAssigned hooks exist
4754
+ await customElements.assignFeatures(MyElement, {
4755
+ roundabout: {
4756
+ spawn: RoundaboutFeature,
4757
+ customData: raConfig
4758
+ }
4759
+ });
4760
+
4761
+ // Now define — class is fully set up, connectedCallback will be synchronous
4762
+ customElements.define('my-element', MyElement);
4763
+ ```
4764
+
4765
+ **How it works:**
4766
+
4767
+ - `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
4768
+ - If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
4769
+ - If `onAssigned` returns a Promise, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
4770
+ - If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible — existing code that doesn't `await` still works).
4771
+ - 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.
4772
+
4773
+ **`await` is always safe:**
4774
+
4775
+ ```JavaScript
4776
+ // These are equivalent for sync features (no onAssigned or sync onAssigned):
4777
+ customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4778
+ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
4779
+ // Both work — await on undefined is a no-op
4780
+ ```
4781
+
4715
4782
  ### Roadmap (future phases)
4716
4783
 
4717
4784
  - **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
package/assignFeatures.js CHANGED
@@ -383,6 +383,7 @@ export function assignFeatures(ctr, features, featuresRegistry) {
383
383
  if (!supportedFeatures) {
384
384
  throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
385
385
  }
386
+ const onAssignedPromises = [];
386
387
  for (const key of Object.keys(features)) {
387
388
  // 1. Confirm the key is opted-in via supportedFeatures
388
389
  if (!(key in supportedFeatures)) {
@@ -401,13 +402,28 @@ export function assignFeatures(ctr, features, featuresRegistry) {
401
402
  featuresRegistry.set(ctr, key, features[key]);
402
403
  // 5. Install the lazy getter on the prototype
403
404
  installFeatureGetter(ctr, key, featuresRegistry);
404
- // 6. Install callback forwarding if configured
405
+ // 6. Install callback forwarding if configured (merge author + consumer)
405
406
  const featureConfig = features[key];
406
- if (featureConfig.callbackForwarding && featureConfig.callbackForwarding.length > 0) {
407
- installCallbackForwarding(ctr, key, featureConfig.callbackForwarding);
407
+ const optIn = supportedFeatures[key];
408
+ const authorCallbacks = optIn.callbackForwarding || [];
409
+ const consumerCallbacks = featureConfig.callbackForwarding || [];
410
+ // Union of both (author defaults + consumer additions)
411
+ const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
412
+ if (allCallbacks.length > 0) {
413
+ installCallbackForwarding(ctr, key, allCallbacks);
414
+ }
415
+ // 7. Call static onAssigned if the spawn class defines it
416
+ const SpawnClass = featureConfig.spawn;
417
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
418
+ Object.hasOwn(SpawnClass, 'onAssigned') &&
419
+ typeof SpawnClass.onAssigned === 'function') {
420
+ const result = SpawnClass.onAssigned(ctr, featureConfig);
421
+ if (result && typeof result.then === 'function') {
422
+ onAssignedPromises.push(result);
423
+ }
408
424
  }
409
425
  }
410
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
426
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
411
427
  const featuresConfig = ctr.featuresConfig;
412
428
  if (featuresConfig?.lifecycleKeys) {
413
429
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -415,6 +431,11 @@ export function assignFeatures(ctr, features, featuresRegistry) {
415
431
  installWhenFeatureReadyMethod(ctr, methodName);
416
432
  }
417
433
  }
434
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
435
+ if (onAssignedPromises.length > 0) {
436
+ return Promise.all(onAssignedPromises).then(() => { });
437
+ }
438
+ return undefined;
418
439
  }
419
440
  /**
420
441
  * Captures own-properties that shadow feature getters and stores them as initVals.
@@ -520,7 +541,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
520
541
  });
521
542
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
522
543
  value: function (ctr, features) {
523
- assignFeatures(ctr, features, this.featuresRegistry);
544
+ return assignFeatures(ctr, features, this.featuresRegistry);
524
545
  },
525
546
  writable: true,
526
547
  enumerable: false,
package/assignFeatures.ts CHANGED
@@ -65,6 +65,16 @@ export interface SupportedFeatureConfig {
65
65
  * }
66
66
  */
67
67
  getSharedContext?: (instance: any) => any;
68
+
69
+ /**
70
+ * Lifecycle callbacks that this feature requires.
71
+ * Serves as the default — the consumer can add more via FeatureConfig.callbackForwarding
72
+ * but cannot remove these.
73
+ *
74
+ * Supported: 'connectedCallback', 'disconnectedCallback',
75
+ * 'attributeChangedCallback', 'adoptedCallback'
76
+ */
77
+ callbackForwarding?: string[];
68
78
  }
69
79
 
70
80
  /**
@@ -597,7 +607,7 @@ export function assignFeatures(
597
607
  ctr: Function,
598
608
  features: FeatureConfigsMap,
599
609
  featuresRegistry: FeaturesRegistry
600
- ): void {
610
+ ): Promise<void> | undefined {
601
611
  // Validate that the constructor has static supportedFeatures
602
612
  const supportedFeatures: SupportedFeaturesMap | undefined = (ctr as any).supportedFeatures;
603
613
 
@@ -607,6 +617,8 @@ export function assignFeatures(
607
617
  );
608
618
  }
609
619
 
620
+ const onAssignedPromises: Promise<void>[] = [];
621
+
610
622
  for (const key of Object.keys(features)) {
611
623
  // 1. Confirm the key is opted-in via supportedFeatures
612
624
  if (!(key in supportedFeatures)) {
@@ -636,14 +648,30 @@ export function assignFeatures(
636
648
  // 5. Install the lazy getter on the prototype
637
649
  installFeatureGetter(ctr, key, featuresRegistry);
638
650
 
639
- // 6. Install callback forwarding if configured
651
+ // 6. Install callback forwarding if configured (merge author + consumer)
640
652
  const featureConfig = features[key];
641
- if (featureConfig.callbackForwarding && featureConfig.callbackForwarding.length > 0) {
642
- installCallbackForwarding(ctr, key, featureConfig.callbackForwarding);
653
+ const optIn = supportedFeatures[key];
654
+ const authorCallbacks = optIn.callbackForwarding || [];
655
+ const consumerCallbacks = featureConfig.callbackForwarding || [];
656
+ // Union of both (author defaults + consumer additions)
657
+ const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
658
+ if (allCallbacks.length > 0) {
659
+ installCallbackForwarding(ctr, key, allCallbacks);
660
+ }
661
+
662
+ // 7. Call static onAssigned if the spawn class defines it
663
+ const SpawnClass = featureConfig.spawn;
664
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
665
+ Object.hasOwn(SpawnClass as any, 'onAssigned') &&
666
+ typeof (SpawnClass as any).onAssigned === 'function') {
667
+ const result = (SpawnClass as any).onAssigned(ctr, featureConfig);
668
+ if (result && typeof result.then === 'function') {
669
+ onAssignedPromises.push(result);
670
+ }
643
671
  }
644
672
  }
645
673
 
646
- // 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
674
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
647
675
  const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
648
676
  if (featuresConfig?.lifecycleKeys) {
649
677
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -651,6 +679,12 @@ export function assignFeatures(
651
679
  installWhenFeatureReadyMethod(ctr, methodName);
652
680
  }
653
681
  }
682
+
683
+ // Return a Promise if any onAssigned hooks are async, otherwise undefined
684
+ if (onAssignedPromises.length > 0) {
685
+ return Promise.all(onAssignedPromises).then(() => {});
686
+ }
687
+ return undefined;
654
688
  }
655
689
 
656
690
  /**
@@ -757,7 +791,7 @@ export class PropertyBag {
757
791
  declare global {
758
792
  interface CustomElementRegistry {
759
793
  featuresRegistry: FeaturesRegistry;
760
- assignFeatures(ctr: Function, features: FeatureConfigsMap): void;
794
+ assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
761
795
  }
762
796
  }
763
797
 
@@ -778,8 +812,8 @@ if (typeof CustomElementRegistry !== 'undefined') {
778
812
  });
779
813
 
780
814
  Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
781
- value: function (ctr: Function, features: FeatureConfigsMap): void {
782
- assignFeatures(ctr, features, this.featuresRegistry);
815
+ value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
816
+ return assignFeatures(ctr, features, this.featuresRegistry);
783
817
  },
784
818
  writable: true,
785
819
  enumerable: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
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": {
@@ -459,4 +459,4 @@ export declare function assignFeatures(
459
459
  ctr: Function,
460
460
  features: FeatureConfigsMap,
461
461
  featuresRegistry: FeaturesRegistry
462
- ): void;
462
+ ): Promise<void> | undefined;