assign-gingerly 0.0.42 → 0.0.44

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
 
@@ -4779,6 +4782,19 @@ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature }
4779
4782
  // Both work — await on undefined is a no-op
4780
4783
  ```
4781
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
+ | [roundabout](https://www.npmjs.com/package/roundabout) | Reactive view-model binding with template rendering and computed property orchestration | [GitHub](https://github.com/bahrus/roundabout#using-roundaboutfeature-with-assignfeatures) |
4794
+ | [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) |
4795
+
4796
+ </details>
4797
+
4782
4798
  ### Roadmap (future phases)
4783
4799
 
4784
4800
  - **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,
@@ -476,6 +479,86 @@ export function captureFeatureInitVals(instance) {
476
479
  }
477
480
  }
478
481
  }
482
+ /**
483
+ * Global store for inter-feature suggestions.
484
+ * Structure: Map<targetSymbol, Map<targetClass, Array<FeatureInfoSuggestion>>>
485
+ * Scoped per target class to prevent leaking between different custom elements.
486
+ */
487
+ const featureInfoSuggestions = new Map();
488
+ /**
489
+ * Suggest configuration to another feature during registration.
490
+ *
491
+ * Call this in a feature's `static onAssigned` to provide config fragments
492
+ * (withAttrs, customData) that another feature should merge into its own config.
493
+ *
494
+ * The target feature is identified by a Symbol (stable across versions and mocks).
495
+ * Suggestions are scoped per target class to prevent leaking between different
496
+ * custom elements that use the same features.
497
+ *
498
+ * @param fromFeatureCtr - The feature class making the suggestion (for tracing)
499
+ * @param toFeatureSymbol - Symbol identifying the target feature
500
+ * @param featureInfo - Config fragments to suggest (withAttrs, customData)
501
+ * @param targetClass - The custom element class being configured
502
+ *
503
+ * @example
504
+ * import { suggestFeatureInfo } from 'assign-gingerly/assignFeatures.js';
505
+ * import { ROUNDABOUT_FEATURE } from 'roundabout/symbols.js';
506
+ *
507
+ * class FaceUp {
508
+ * static onAssigned(ctr, featureConfig) {
509
+ * suggestFeatureInfo(FaceUp, ROUNDABOUT_FEATURE, {
510
+ * customData: { formBindings: { value: 'value' } }
511
+ * }, ctr);
512
+ * }
513
+ * }
514
+ */
515
+ export function suggestFeatureInfo(fromFeatureCtr, toFeatureSymbol, featureInfo, targetClass) {
516
+ let symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
517
+ if (!symbolMap) {
518
+ symbolMap = new Map();
519
+ featureInfoSuggestions.set(toFeatureSymbol, symbolMap);
520
+ }
521
+ let suggestions = symbolMap.get(targetClass);
522
+ if (!suggestions) {
523
+ suggestions = [];
524
+ symbolMap.set(targetClass, suggestions);
525
+ }
526
+ suggestions.push({
527
+ from: fromFeatureCtr,
528
+ ...featureInfo
529
+ });
530
+ }
531
+ /**
532
+ * Retrieve suggestions made to a feature by other features.
533
+ *
534
+ * Call this in a feature's `static onAssigned` to read config fragments
535
+ * suggested by other features that were processed earlier.
536
+ *
537
+ * @param toFeatureSymbol - Symbol identifying this feature (the target)
538
+ * @param targetClass - The custom element class being configured
539
+ * @returns Array of suggestions (empty if none)
540
+ *
541
+ * @example
542
+ * import { getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';
543
+ * import { ROUNDABOUT_FEATURE } from './symbols.js';
544
+ *
545
+ * class RoundaboutFeature {
546
+ * static onAssigned(ctr, featureConfig) {
547
+ * const suggestions = getFeatureInfoSuggestions(ROUNDABOUT_FEATURE, ctr);
548
+ * for (const suggestion of suggestions) {
549
+ * if (suggestion.customData) {
550
+ * featureConfig.customData = { ...featureConfig.customData, ...suggestion.customData };
551
+ * }
552
+ * }
553
+ * }
554
+ * }
555
+ */
556
+ export function getFeatureInfoSuggestions(toFeatureSymbol, targetClass) {
557
+ const symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
558
+ if (!symbolMap)
559
+ return [];
560
+ return symbolMap.get(targetClass) || [];
561
+ }
479
562
  // =============================================================================
480
563
  // PropertyBag — base class for nested feature containers
481
564
  // =============================================================================
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
  /**
@@ -728,6 +731,114 @@ export function captureFeatureInitVals(instance: any): void {
728
731
  }
729
732
  }
730
733
 
734
+ // =============================================================================
735
+ // Inter-feature communication: suggestFeatureInfo / getFeatureInfoSuggestions
736
+ // =============================================================================
737
+
738
+ /**
739
+ * A suggestion from one feature to another, containing config fragments to merge.
740
+ */
741
+ export interface FeatureInfoSuggestion {
742
+ /** The feature class that made the suggestion (for debugging/tracing) */
743
+ from: Function;
744
+ /** Attribute patterns to merge into the target feature's withAttrs */
745
+ withAttrs?: any;
746
+ /** Custom data to merge into the target feature's customData */
747
+ customData?: any;
748
+ }
749
+
750
+ /**
751
+ * Global store for inter-feature suggestions.
752
+ * Structure: Map<targetSymbol, Map<targetClass, Array<FeatureInfoSuggestion>>>
753
+ * Scoped per target class to prevent leaking between different custom elements.
754
+ */
755
+ const featureInfoSuggestions = new Map<symbol, Map<Function, FeatureInfoSuggestion[]>>();
756
+
757
+ /**
758
+ * Suggest configuration to another feature during registration.
759
+ *
760
+ * Call this in a feature's `static onAssigned` to provide config fragments
761
+ * (withAttrs, customData) that another feature should merge into its own config.
762
+ *
763
+ * The target feature is identified by a Symbol (stable across versions and mocks).
764
+ * Suggestions are scoped per target class to prevent leaking between different
765
+ * custom elements that use the same features.
766
+ *
767
+ * @param fromFeatureCtr - The feature class making the suggestion (for tracing)
768
+ * @param toFeatureSymbol - Symbol identifying the target feature
769
+ * @param featureInfo - Config fragments to suggest (withAttrs, customData)
770
+ * @param targetClass - The custom element class being configured
771
+ *
772
+ * @example
773
+ * import { suggestFeatureInfo } from 'assign-gingerly/assignFeatures.js';
774
+ * import { ROUNDABOUT_FEATURE } from 'roundabout/symbols.js';
775
+ *
776
+ * class FaceUp {
777
+ * static onAssigned(ctr, featureConfig) {
778
+ * suggestFeatureInfo(FaceUp, ROUNDABOUT_FEATURE, {
779
+ * customData: { formBindings: { value: 'value' } }
780
+ * }, ctr);
781
+ * }
782
+ * }
783
+ */
784
+ export function suggestFeatureInfo(
785
+ fromFeatureCtr: Function,
786
+ toFeatureSymbol: symbol,
787
+ featureInfo: { withAttrs?: any; customData?: any },
788
+ targetClass: Function
789
+ ): void {
790
+ let symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
791
+ if (!symbolMap) {
792
+ symbolMap = new Map();
793
+ featureInfoSuggestions.set(toFeatureSymbol, symbolMap);
794
+ }
795
+
796
+ let suggestions = symbolMap.get(targetClass);
797
+ if (!suggestions) {
798
+ suggestions = [];
799
+ symbolMap.set(targetClass, suggestions);
800
+ }
801
+
802
+ suggestions.push({
803
+ from: fromFeatureCtr,
804
+ ...featureInfo
805
+ });
806
+ }
807
+
808
+ /**
809
+ * Retrieve suggestions made to a feature by other features.
810
+ *
811
+ * Call this in a feature's `static onAssigned` to read config fragments
812
+ * suggested by other features that were processed earlier.
813
+ *
814
+ * @param toFeatureSymbol - Symbol identifying this feature (the target)
815
+ * @param targetClass - The custom element class being configured
816
+ * @returns Array of suggestions (empty if none)
817
+ *
818
+ * @example
819
+ * import { getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';
820
+ * import { ROUNDABOUT_FEATURE } from './symbols.js';
821
+ *
822
+ * class RoundaboutFeature {
823
+ * static onAssigned(ctr, featureConfig) {
824
+ * const suggestions = getFeatureInfoSuggestions(ROUNDABOUT_FEATURE, ctr);
825
+ * for (const suggestion of suggestions) {
826
+ * if (suggestion.customData) {
827
+ * featureConfig.customData = { ...featureConfig.customData, ...suggestion.customData };
828
+ * }
829
+ * }
830
+ * }
831
+ * }
832
+ */
833
+ export function getFeatureInfoSuggestions(
834
+ toFeatureSymbol: symbol,
835
+ targetClass: Function
836
+ ): FeatureInfoSuggestion[] {
837
+ const symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
838
+ if (!symbolMap) return [];
839
+ return symbolMap.get(targetClass) || [];
840
+ }
841
+
731
842
  // =============================================================================
732
843
  // PropertyBag — base class for nested feature containers
733
844
  // =============================================================================
package/index.js CHANGED
@@ -9,6 +9,6 @@ export { resolveTemplate } from './resolveTemplate.js';
9
9
  export { getHost } from './getHost.js';
10
10
  export { resolveValues, resolveValue } from './resolveValues.js';
11
11
  export { assignFrom } from './assignFrom.js';
12
- export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag } from './assignFeatures.js';
12
+ export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
13
13
  export { installForwarding } from './installForwarding.js';
14
14
  import './object-extension.js';
package/index.ts CHANGED
@@ -9,6 +9,6 @@ export {resolveTemplate} from './resolveTemplate.js';
9
9
  export {getHost} from './getHost.js';
10
10
  export {resolveValues, resolveValue} from './resolveValues.js';
11
11
  export {assignFrom} from './assignFrom.js';
12
- export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag} from './assignFeatures.js';
12
+ export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
13
13
  export {installForwarding} from './installForwarding.js';
14
14
  import './object-extension.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
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": {
@@ -89,7 +89,7 @@
89
89
  "devDependencies": {
90
90
  "@playwright/test": "1.60.0",
91
91
  "spa-ssi": "0.0.27",
92
- "@types/node": "25.7.0",
92
+ "@types/node": "25.8.0",
93
93
  "typescript": "6.0.3"
94
94
  }
95
95
  }
@@ -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
- ): Promise<void> | undefined;