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 +69 -2
- package/assignFeatures.js +26 -5
- package/assignFeatures.ts +42 -8
- package/package.json +1 -1
- package/types/assign-gingerly/types.d.ts +1 -1
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: ['
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
//
|
|
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