assign-gingerly 0.0.37 → 0.0.39
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 +897 -11
- package/assignFeatures.js +457 -0
- package/assignFeatures.ts +683 -0
- package/assignGingerly.js +96 -3
- package/assignGingerly.ts +122 -3
- package/evaluatePathWithAsyncMethods.js +90 -0
- package/evaluatePathWithAsyncMethods.ts +102 -0
- package/index.js +3 -1
- package/index.ts +3 -1
- package/installForwarding.js +61 -0
- package/installForwarding.ts +71 -0
- package/object-extension.js +2 -2
- package/object-extension.ts +3 -3
- package/package.json +14 -2
- package/resolveValues.js +62 -2
- package/resolveValues.ts +71 -2
- package/types/assign-gingerly/types.d.ts +146 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFeatures - Dependency injection for custom element features
|
|
3
|
+
*
|
|
4
|
+
* Allows custom element authors to declare supported feature slots via
|
|
5
|
+
* `static supportedFeatures`, then have implementations injected via
|
|
6
|
+
* `customElementRegistry.assignFeatures(Ctor, features)`.
|
|
7
|
+
*
|
|
8
|
+
* Features are lazily instantiated on first property access via getter-only
|
|
9
|
+
* properties installed on the class prototype.
|
|
10
|
+
*/
|
|
11
|
+
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
12
|
+
/**
|
|
13
|
+
* WeakMap storing per-instance feature caches.
|
|
14
|
+
* Outer key: the instance (element or other object).
|
|
15
|
+
* Inner map: feature key -> spawned instance.
|
|
16
|
+
*/
|
|
17
|
+
const featureStorage = new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* The features registry: maps a constructor to its accumulated feature injections.
|
|
20
|
+
*/
|
|
21
|
+
export class FeaturesRegistry {
|
|
22
|
+
#registry = new Map();
|
|
23
|
+
has(ctr) {
|
|
24
|
+
return this.#registry.has(ctr);
|
|
25
|
+
}
|
|
26
|
+
get(ctr) {
|
|
27
|
+
return this.#registry.get(ctr);
|
|
28
|
+
}
|
|
29
|
+
set(ctr, key, injection) {
|
|
30
|
+
let features = this.#registry.get(ctr);
|
|
31
|
+
if (!features) {
|
|
32
|
+
features = new Map();
|
|
33
|
+
this.#registry.set(ctr, features);
|
|
34
|
+
}
|
|
35
|
+
features.set(key, injection);
|
|
36
|
+
}
|
|
37
|
+
hasKey(ctr, key) {
|
|
38
|
+
const features = this.#registry.get(ctr);
|
|
39
|
+
return features ? features.has(key) : false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sentinel symbol to mark stored values as raw initVals (not yet spawned).
|
|
44
|
+
*/
|
|
45
|
+
const RAW_INIT_VALS = Symbol('rawInitVals');
|
|
46
|
+
/**
|
|
47
|
+
* Sentinel symbol to mark stored values as error state from failed async spawn.
|
|
48
|
+
*/
|
|
49
|
+
const FEATURE_ERROR = Symbol('featureError');
|
|
50
|
+
/**
|
|
51
|
+
* WeakMap storing pending Promises for async feature resolution.
|
|
52
|
+
* Outer key: the instance. Inner map: feature key -> { promise, resolve, reject }.
|
|
53
|
+
*/
|
|
54
|
+
const pendingFeatures = new WeakMap();
|
|
55
|
+
/**
|
|
56
|
+
* Resolves the whenFeatureReady method name from lifecycleKeys config.
|
|
57
|
+
* Returns undefined if lifecycleKeys is not set.
|
|
58
|
+
*/
|
|
59
|
+
function resolveWhenFeatureReadyName(lifecycleKeys) {
|
|
60
|
+
if (lifecycleKeys === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (lifecycleKeys === true)
|
|
63
|
+
return 'whenFeatureReady';
|
|
64
|
+
return lifecycleKeys.whenFeatureReady || 'whenFeatureReady';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Installs the whenFeatureReady method on the constructor prototype if not already present.
|
|
68
|
+
*/
|
|
69
|
+
function installWhenFeatureReadyMethod(ctr, methodName) {
|
|
70
|
+
// Only install once per class
|
|
71
|
+
if (Object.getOwnPropertyDescriptor(ctr.prototype, methodName))
|
|
72
|
+
return;
|
|
73
|
+
Object.defineProperty(ctr.prototype, methodName, {
|
|
74
|
+
value: function (featureKey) {
|
|
75
|
+
// Trigger the getter (starts async resolution if needed, or returns sync instance)
|
|
76
|
+
const current = this[featureKey];
|
|
77
|
+
// Check if there's a pending async resolution for this instance + key
|
|
78
|
+
const pending = pendingFeatures.get(this)?.get(featureKey);
|
|
79
|
+
if (pending) {
|
|
80
|
+
return pending.promise;
|
|
81
|
+
}
|
|
82
|
+
// No pending — feature is already resolved (sync or async already completed)
|
|
83
|
+
return Promise.resolve(current);
|
|
84
|
+
},
|
|
85
|
+
writable: true,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
configurable: true
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Determines if a function is an async spawner (returns a Promise<Constructor>)
|
|
92
|
+
* rather than a synchronous constructor.
|
|
93
|
+
*
|
|
94
|
+
* Heuristic:
|
|
95
|
+
* - AsyncFunction (async () => ...) → async spawner
|
|
96
|
+
* - Arrow function (no .prototype) → async spawner (assumed to return Promise<Constructor>)
|
|
97
|
+
* - Class or function declaration (has .prototype) → synchronous constructor
|
|
98
|
+
*/
|
|
99
|
+
function isAsyncSpawn(fn) {
|
|
100
|
+
if (typeof fn !== 'function')
|
|
101
|
+
return false;
|
|
102
|
+
// Explicit async function
|
|
103
|
+
if (fn.constructor.name === 'AsyncFunction')
|
|
104
|
+
return true;
|
|
105
|
+
// Arrow function or non-constructor function (no .prototype)
|
|
106
|
+
if (fn.prototype === undefined)
|
|
107
|
+
return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Installs a getter/setter pair on the constructor's prototype for the given feature key.
|
|
112
|
+
*
|
|
113
|
+
* - The setter stores raw values (pre-upgrade or early assignment) into the WeakMap
|
|
114
|
+
* tagged with a sentinel so the getter knows they are initVals, not spawned instances.
|
|
115
|
+
* - The getter spawns the feature instance on first access, using any stored raw value
|
|
116
|
+
* as initVals, then caches the spawned instance.
|
|
117
|
+
*
|
|
118
|
+
* If an own-property with the same key exists on the instance (e.g., set before
|
|
119
|
+
* the element upgraded), it is captured as initVals and deleted so the prototype
|
|
120
|
+
* getter/setter is no longer shadowed.
|
|
121
|
+
*/
|
|
122
|
+
function installFeatureGetter(ctr, key, featuresRegistry) {
|
|
123
|
+
Object.defineProperty(ctr.prototype, key, {
|
|
124
|
+
get: function () {
|
|
125
|
+
// Get or create the per-instance storage
|
|
126
|
+
let storage = featureStorage.get(this);
|
|
127
|
+
if (!storage) {
|
|
128
|
+
storage = new Map();
|
|
129
|
+
featureStorage.set(this, storage);
|
|
130
|
+
}
|
|
131
|
+
const stored = storage.get(key);
|
|
132
|
+
// Check for error state from failed async spawn
|
|
133
|
+
if (stored && typeof stored === 'object' && FEATURE_ERROR in stored) {
|
|
134
|
+
throw stored[FEATURE_ERROR];
|
|
135
|
+
}
|
|
136
|
+
// If already spawned (not a raw sentinel, not undefined), return it
|
|
137
|
+
if (stored !== undefined && !(stored && typeof stored === 'object' && RAW_INIT_VALS in stored)) {
|
|
138
|
+
return stored;
|
|
139
|
+
}
|
|
140
|
+
// Determine initVals: from setter-stored raw value, or from own-property shadow
|
|
141
|
+
let initVals = undefined;
|
|
142
|
+
if (stored && typeof stored === 'object' && RAW_INIT_VALS in stored) {
|
|
143
|
+
initVals = stored[RAW_INIT_VALS];
|
|
144
|
+
storage.delete(key);
|
|
145
|
+
}
|
|
146
|
+
else if (Object.hasOwn(this, key)) {
|
|
147
|
+
initVals = this[key];
|
|
148
|
+
delete this[key]; // Unshadow the prototype accessor
|
|
149
|
+
}
|
|
150
|
+
// Resolve the registry — use scoped registry if available, fall back to global
|
|
151
|
+
const registry = (this.customElementRegistry || customElements);
|
|
152
|
+
const fr = registry.featuresRegistry;
|
|
153
|
+
if (!fr || !fr.has(ctr)) {
|
|
154
|
+
throw new Error(`assignFeatures: featuresRegistry missing entry for constructor`);
|
|
155
|
+
}
|
|
156
|
+
const features = fr.get(ctr);
|
|
157
|
+
const injection = features.get(key);
|
|
158
|
+
if (!injection) {
|
|
159
|
+
throw new Error(`assignFeatures: no injection found for feature "${key}"`);
|
|
160
|
+
}
|
|
161
|
+
// Resolve spawn: injection.spawn takes priority, then fallbackSpawn
|
|
162
|
+
const supportedFeatures = ctr.supportedFeatures;
|
|
163
|
+
const optIn = supportedFeatures?.[key];
|
|
164
|
+
if (!optIn) {
|
|
165
|
+
throw new Error(`assignFeatures: "${key}" not in static supportedFeatures`);
|
|
166
|
+
}
|
|
167
|
+
const SpawnClass = injection.spawn || optIn.fallbackSpawn;
|
|
168
|
+
if (!SpawnClass) {
|
|
169
|
+
throw new Error(`assignFeatures: no spawn implementation found for feature "${key}". ` +
|
|
170
|
+
`Provide spawn in assignFeatures() or fallbackSpawn in supportedFeatures.`);
|
|
171
|
+
}
|
|
172
|
+
// Build the spawn context
|
|
173
|
+
const shared = optIn.getSharedContext?.(this);
|
|
174
|
+
const ctx = {
|
|
175
|
+
key,
|
|
176
|
+
optIn,
|
|
177
|
+
injection,
|
|
178
|
+
featuresRegistry: fr,
|
|
179
|
+
shared
|
|
180
|
+
};
|
|
181
|
+
// Parse attributes if withAttrs is configured
|
|
182
|
+
let attrInitVals = undefined;
|
|
183
|
+
if (injection.withAttrs && this instanceof Element) {
|
|
184
|
+
try {
|
|
185
|
+
attrInitVals = parseWithAttrs(this, injection.withAttrs, true // always unprefixed for features
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.error('Error parsing feature attributes:', e);
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Merge: attributes are base layer, programmatic initVals override
|
|
194
|
+
if (attrInitVals) {
|
|
195
|
+
initVals = initVals
|
|
196
|
+
? { ...attrInitVals, ...initVals }
|
|
197
|
+
: attrInitVals;
|
|
198
|
+
}
|
|
199
|
+
if (isAsyncSpawn(SpawnClass)) {
|
|
200
|
+
// Async path: SpawnClass is a function that returns Promise<Constructor>
|
|
201
|
+
const placeholder = initVals && typeof initVals === 'object' ? initVals : {};
|
|
202
|
+
storage.set(key, placeholder);
|
|
203
|
+
// Capture host element reference for the async callback
|
|
204
|
+
const hostElement = this;
|
|
205
|
+
// Create a pending Promise for whenFeatureReady consumers
|
|
206
|
+
let pendingMap = pendingFeatures.get(hostElement);
|
|
207
|
+
if (!pendingMap) {
|
|
208
|
+
pendingMap = new Map();
|
|
209
|
+
pendingFeatures.set(hostElement, pendingMap);
|
|
210
|
+
}
|
|
211
|
+
let resolvePending;
|
|
212
|
+
let rejectPending;
|
|
213
|
+
const promise = new Promise((resolve, reject) => {
|
|
214
|
+
resolvePending = resolve;
|
|
215
|
+
rejectPending = reject;
|
|
216
|
+
});
|
|
217
|
+
pendingMap.set(key, { promise, resolve: resolvePending, reject: rejectPending });
|
|
218
|
+
// Kick off async resolution
|
|
219
|
+
SpawnClass().then((ResolvedClass) => {
|
|
220
|
+
// Mutate injection so future getter calls see the resolved constructor
|
|
221
|
+
injection.spawn = ResolvedClass;
|
|
222
|
+
// Get the current placeholder (may have accumulated properties via assignGingerly)
|
|
223
|
+
const currentStorage = featureStorage.get(hostElement);
|
|
224
|
+
const currentPlaceholder = currentStorage?.get(key);
|
|
225
|
+
// Don't upgrade if an error was stored or if already upgraded
|
|
226
|
+
if (!currentPlaceholder || (typeof currentPlaceholder === 'object' && FEATURE_ERROR in currentPlaceholder)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Parse attributes at resolution time (element should be in DOM by now)
|
|
230
|
+
let asyncAttrInitVals = undefined;
|
|
231
|
+
if (injection.withAttrs && hostElement instanceof Element) {
|
|
232
|
+
try {
|
|
233
|
+
asyncAttrInitVals = parseWithAttrs(hostElement, injection.withAttrs, true // always unprefixed for features
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
// Non-fatal: log and continue with placeholder as initVals
|
|
238
|
+
console.error('Error parsing feature attributes during async resolution:', e);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Merge: attributes are base, placeholder (programmatic) overrides
|
|
242
|
+
const asyncInitVals = asyncAttrInitVals
|
|
243
|
+
? { ...asyncAttrInitVals, ...currentPlaceholder }
|
|
244
|
+
: currentPlaceholder;
|
|
245
|
+
// Instantiate the real class with merged initVals
|
|
246
|
+
const realCtx = {
|
|
247
|
+
key,
|
|
248
|
+
optIn,
|
|
249
|
+
injection,
|
|
250
|
+
featuresRegistry: fr,
|
|
251
|
+
shared: optIn.getSharedContext?.(hostElement)
|
|
252
|
+
};
|
|
253
|
+
const instance = new ResolvedClass(hostElement, realCtx, asyncInitVals);
|
|
254
|
+
// Validate shape if configured
|
|
255
|
+
if (optIn.validateShape) {
|
|
256
|
+
if (!optIn.validateShape(instance)) {
|
|
257
|
+
const error = new Error(`assignFeatures: spawned instance for "${key}" failed shape validation`);
|
|
258
|
+
error.placeholder = currentPlaceholder;
|
|
259
|
+
currentStorage.set(key, { [FEATURE_ERROR]: error });
|
|
260
|
+
rejectPending(error);
|
|
261
|
+
pendingMap.delete(key);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Replace placeholder with real instance
|
|
266
|
+
currentStorage.set(key, instance);
|
|
267
|
+
// Resolve the pending Promise and clean up
|
|
268
|
+
resolvePending(instance);
|
|
269
|
+
pendingMap.delete(key);
|
|
270
|
+
}).catch((err) => {
|
|
271
|
+
// Store error state — getter will throw on next access
|
|
272
|
+
const currentStorage = featureStorage.get(hostElement);
|
|
273
|
+
const currentPlaceholder = currentStorage?.get(key);
|
|
274
|
+
const error = new Error(`assignFeatures: async spawn for "${key}" failed: ${err.message}`);
|
|
275
|
+
error.placeholder = currentPlaceholder;
|
|
276
|
+
error.cause = err;
|
|
277
|
+
currentStorage?.set(key, { [FEATURE_ERROR]: error });
|
|
278
|
+
// Reject the pending Promise and clean up
|
|
279
|
+
rejectPending(error);
|
|
280
|
+
pendingMap.delete(key);
|
|
281
|
+
});
|
|
282
|
+
return placeholder;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// Synchronous path: SpawnClass is a constructor
|
|
286
|
+
const instance = new SpawnClass(this, ctx, initVals);
|
|
287
|
+
// Validate shape if configured
|
|
288
|
+
if (optIn.validateShape) {
|
|
289
|
+
if (!optIn.validateShape(instance)) {
|
|
290
|
+
throw new Error(`assignFeatures: spawned instance for "${key}" failed shape validation`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
storage.set(key, instance);
|
|
294
|
+
return instance;
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
enumerable: true,
|
|
298
|
+
configurable: false
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Core assignFeatures implementation.
|
|
303
|
+
* Validates inputs, registers injections, and installs lazy getters.
|
|
304
|
+
*
|
|
305
|
+
* Important: Call assignFeatures BEFORE customElements.define(), or at minimum
|
|
306
|
+
* before any instances of the element are created. The lazy getters must be on
|
|
307
|
+
* the prototype before instances exist to properly capture pre-set properties.
|
|
308
|
+
*
|
|
309
|
+
* @param ctr - The constructor (class) to assign features to
|
|
310
|
+
* @param features - Map of feature keys to their injection configs
|
|
311
|
+
* @param featuresRegistry - The registry to store injections in
|
|
312
|
+
*/
|
|
313
|
+
export function assignFeatures(ctr, features, featuresRegistry) {
|
|
314
|
+
// Validate that the constructor has static supportedFeatures
|
|
315
|
+
const supportedFeatures = ctr.supportedFeatures;
|
|
316
|
+
if (!supportedFeatures) {
|
|
317
|
+
throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
|
|
318
|
+
}
|
|
319
|
+
for (const key of Object.keys(features)) {
|
|
320
|
+
// 1. Confirm the key is opted-in via supportedFeatures
|
|
321
|
+
if (!(key in supportedFeatures)) {
|
|
322
|
+
throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
|
|
323
|
+
}
|
|
324
|
+
// 2. Check that the prototype doesn't already have this property defined
|
|
325
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
326
|
+
if (existingDescriptor) {
|
|
327
|
+
throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
|
|
328
|
+
}
|
|
329
|
+
// 3. Check that this key hasn't already been registered for this constructor
|
|
330
|
+
if (featuresRegistry.hasKey(ctr, key)) {
|
|
331
|
+
throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
|
|
332
|
+
}
|
|
333
|
+
// 4. Register the injection
|
|
334
|
+
featuresRegistry.set(ctr, key, features[key]);
|
|
335
|
+
// 5. Install the lazy getter on the prototype
|
|
336
|
+
installFeatureGetter(ctr, key, featuresRegistry);
|
|
337
|
+
}
|
|
338
|
+
// 6. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
|
|
339
|
+
const featuresConfig = ctr.featuresConfig;
|
|
340
|
+
if (featuresConfig?.lifecycleKeys) {
|
|
341
|
+
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
342
|
+
if (methodName) {
|
|
343
|
+
installWhenFeatureReadyMethod(ctr, methodName);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Captures own-properties that shadow feature getters and stores them as initVals.
|
|
349
|
+
* Call this in the custom element constructor to handle pre-upgrade property values.
|
|
350
|
+
*
|
|
351
|
+
* When an element exists in the DOM before `define()` is called, properties may have
|
|
352
|
+
* been set on it directly. After upgrade, these own-properties shadow the prototype
|
|
353
|
+
* getters installed by `assignFeatures`. This helper captures those values and deletes
|
|
354
|
+
* the own-properties so the getters can function properly.
|
|
355
|
+
*
|
|
356
|
+
* @param instance - The custom element instance (typically `this` in the constructor)
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* class ClubMember extends HTMLElement {
|
|
360
|
+
* static supportedFeatures = { photoTaker: { fallbackSpawn: PhotoTakerImpl } }
|
|
361
|
+
* constructor() {
|
|
362
|
+
* super();
|
|
363
|
+
* captureFeatureInitVals(this);
|
|
364
|
+
* }
|
|
365
|
+
* }
|
|
366
|
+
*/
|
|
367
|
+
export function captureFeatureInitVals(instance) {
|
|
368
|
+
const ctr = instance.constructor;
|
|
369
|
+
const supportedFeatures = ctr.supportedFeatures;
|
|
370
|
+
if (!supportedFeatures)
|
|
371
|
+
return;
|
|
372
|
+
for (const key of Object.keys(supportedFeatures)) {
|
|
373
|
+
if (Object.hasOwn(instance, key)) {
|
|
374
|
+
const value = instance[key];
|
|
375
|
+
delete instance[key];
|
|
376
|
+
// Store in the WeakMap so the getter can pick it up
|
|
377
|
+
let storage = featureStorage.get(instance);
|
|
378
|
+
if (!storage) {
|
|
379
|
+
storage = new Map();
|
|
380
|
+
featureStorage.set(instance, storage);
|
|
381
|
+
}
|
|
382
|
+
storage.set(key, { [RAW_INIT_VALS]: value });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// =============================================================================
|
|
387
|
+
// PropertyBag — base class for nested feature containers
|
|
388
|
+
// =============================================================================
|
|
389
|
+
/**
|
|
390
|
+
* PropertyBag is a base class for creating nested feature containers.
|
|
391
|
+
*
|
|
392
|
+
* Subclass it to group related features under a single namespace property.
|
|
393
|
+
* PropertyBag carries the `customElementRegistry` reference from the host element
|
|
394
|
+
* so that nested features can resolve their registries correctly.
|
|
395
|
+
*
|
|
396
|
+
* PropertyBag must be subclassed — direct instantiation throws an error.
|
|
397
|
+
* Subclasses must define `static supportedFeatures` to declare their feature slots.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* class ClubMemberBehaviors extends PropertyBag {
|
|
401
|
+
* static supportedFeatures = {
|
|
402
|
+
* commandBehavior: { fallbackSpawn: CommandFeatureImpl },
|
|
403
|
+
* ariaBehavior: { fallbackSpawn: AriaFeatureImpl }
|
|
404
|
+
* }
|
|
405
|
+
* }
|
|
406
|
+
*
|
|
407
|
+
* class ClubMember extends HTMLElement {
|
|
408
|
+
* static supportedFeatures = {
|
|
409
|
+
* behaviors: { fallbackSpawn: ClubMemberBehaviors }
|
|
410
|
+
* }
|
|
411
|
+
* }
|
|
412
|
+
*
|
|
413
|
+
* customElements.assignFeatures(ClubMember, { behaviors: { spawn: ClubMemberBehaviors } });
|
|
414
|
+
* customElements.assignFeatures(ClubMemberBehaviors, {
|
|
415
|
+
* commandBehavior: { spawn: CommandFeatureImpl }
|
|
416
|
+
* });
|
|
417
|
+
*/
|
|
418
|
+
export class PropertyBag {
|
|
419
|
+
/** Registry reference carried from the host element */
|
|
420
|
+
customElementRegistry;
|
|
421
|
+
constructor(hostElement, ctx, initVals) {
|
|
422
|
+
if (this.constructor === PropertyBag) {
|
|
423
|
+
throw new Error('PropertyBag must be subclassed. Define static supportedFeatures on your subclass.');
|
|
424
|
+
}
|
|
425
|
+
// Carry the registry reference from the host element
|
|
426
|
+
this.customElementRegistry = hostElement.customElementRegistry ||
|
|
427
|
+
(typeof customElements !== 'undefined' ? customElements : undefined);
|
|
428
|
+
// Apply any initVals
|
|
429
|
+
if (initVals && typeof initVals === 'object') {
|
|
430
|
+
Object.assign(this, initVals);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (typeof CustomElementRegistry !== 'undefined') {
|
|
435
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'featuresRegistry', {
|
|
436
|
+
get: function () {
|
|
437
|
+
const registry = new FeaturesRegistry();
|
|
438
|
+
Object.defineProperty(this, 'featuresRegistry', {
|
|
439
|
+
value: registry,
|
|
440
|
+
writable: true,
|
|
441
|
+
enumerable: false,
|
|
442
|
+
configurable: true,
|
|
443
|
+
});
|
|
444
|
+
return registry;
|
|
445
|
+
},
|
|
446
|
+
enumerable: false,
|
|
447
|
+
configurable: true,
|
|
448
|
+
});
|
|
449
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
|
|
450
|
+
value: function (ctr, features) {
|
|
451
|
+
assignFeatures(ctr, features, this.featuresRegistry);
|
|
452
|
+
},
|
|
453
|
+
writable: true,
|
|
454
|
+
enumerable: false,
|
|
455
|
+
configurable: true,
|
|
456
|
+
});
|
|
457
|
+
}
|