assign-gingerly 0.0.41 → 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 +56 -0
- package/assignFeatures.js +18 -2
- package/assignFeatures.ts +24 -5
- package/package.json +1 -1
- package/types/assign-gingerly/types.d.ts +1 -1
package/README.md
CHANGED
|
@@ -4723,6 +4723,62 @@ customElements.assignFeatures(MyElement, {
|
|
|
4723
4723
|
|
|
4724
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.
|
|
4725
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
|
+
|
|
4726
4782
|
### Roadmap (future phases)
|
|
4727
4783
|
|
|
4728
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)) {
|
|
@@ -411,8 +412,18 @@ export function assignFeatures(ctr, features, featuresRegistry) {
|
|
|
411
412
|
if (allCallbacks.length > 0) {
|
|
412
413
|
installCallbackForwarding(ctr, key, allCallbacks);
|
|
413
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
|
+
}
|
|
424
|
+
}
|
|
414
425
|
}
|
|
415
|
-
//
|
|
426
|
+
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
416
427
|
const featuresConfig = ctr.featuresConfig;
|
|
417
428
|
if (featuresConfig?.lifecycleKeys) {
|
|
418
429
|
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
@@ -420,6 +431,11 @@ export function assignFeatures(ctr, features, featuresRegistry) {
|
|
|
420
431
|
installWhenFeatureReadyMethod(ctr, methodName);
|
|
421
432
|
}
|
|
422
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;
|
|
423
439
|
}
|
|
424
440
|
/**
|
|
425
441
|
* Captures own-properties that shadow feature getters and stores them as initVals.
|
|
@@ -525,7 +541,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
|
|
|
525
541
|
});
|
|
526
542
|
Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
|
|
527
543
|
value: function (ctr, features) {
|
|
528
|
-
assignFeatures(ctr, features, this.featuresRegistry);
|
|
544
|
+
return assignFeatures(ctr, features, this.featuresRegistry);
|
|
529
545
|
},
|
|
530
546
|
writable: true,
|
|
531
547
|
enumerable: false,
|
package/assignFeatures.ts
CHANGED
|
@@ -607,7 +607,7 @@ export function assignFeatures(
|
|
|
607
607
|
ctr: Function,
|
|
608
608
|
features: FeatureConfigsMap,
|
|
609
609
|
featuresRegistry: FeaturesRegistry
|
|
610
|
-
): void {
|
|
610
|
+
): Promise<void> | undefined {
|
|
611
611
|
// Validate that the constructor has static supportedFeatures
|
|
612
612
|
const supportedFeatures: SupportedFeaturesMap | undefined = (ctr as any).supportedFeatures;
|
|
613
613
|
|
|
@@ -617,6 +617,8 @@ export function assignFeatures(
|
|
|
617
617
|
);
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
const onAssignedPromises: Promise<void>[] = [];
|
|
621
|
+
|
|
620
622
|
for (const key of Object.keys(features)) {
|
|
621
623
|
// 1. Confirm the key is opted-in via supportedFeatures
|
|
622
624
|
if (!(key in supportedFeatures)) {
|
|
@@ -656,9 +658,20 @@ export function assignFeatures(
|
|
|
656
658
|
if (allCallbacks.length > 0) {
|
|
657
659
|
installCallbackForwarding(ctr, key, allCallbacks);
|
|
658
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
|
+
}
|
|
671
|
+
}
|
|
659
672
|
}
|
|
660
673
|
|
|
661
|
-
//
|
|
674
|
+
// 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
662
675
|
const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
|
|
663
676
|
if (featuresConfig?.lifecycleKeys) {
|
|
664
677
|
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
@@ -666,6 +679,12 @@ export function assignFeatures(
|
|
|
666
679
|
installWhenFeatureReadyMethod(ctr, methodName);
|
|
667
680
|
}
|
|
668
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;
|
|
669
688
|
}
|
|
670
689
|
|
|
671
690
|
/**
|
|
@@ -772,7 +791,7 @@ export class PropertyBag {
|
|
|
772
791
|
declare global {
|
|
773
792
|
interface CustomElementRegistry {
|
|
774
793
|
featuresRegistry: FeaturesRegistry;
|
|
775
|
-
assignFeatures(ctr: Function, features: FeatureConfigsMap): void;
|
|
794
|
+
assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
|
|
776
795
|
}
|
|
777
796
|
}
|
|
778
797
|
|
|
@@ -793,8 +812,8 @@ if (typeof CustomElementRegistry !== 'undefined') {
|
|
|
793
812
|
});
|
|
794
813
|
|
|
795
814
|
Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
|
|
796
|
-
value: function (ctr: Function, features: FeatureConfigsMap): void {
|
|
797
|
-
assignFeatures(ctr, features, this.featuresRegistry);
|
|
815
|
+
value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
|
|
816
|
+
return assignFeatures(ctr, features, this.featuresRegistry);
|
|
798
817
|
},
|
|
799
818
|
writable: true,
|
|
800
819
|
enumerable: false,
|
package/package.json
CHANGED