assign-gingerly 0.0.43 → 0.0.45
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 +26 -1
- package/assignFeatures.js +80 -0
- package/assignFeatures.ts +108 -0
- package/assignGingerly.js +10 -0
- package/assignGingerly.ts +8 -0
- package/assignTentatively.js +16 -2
- package/assignTentatively.ts +14 -2
- package/index.js +1 -1
- package/index.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -751,7 +751,31 @@ console.log(obj);
|
|
|
751
751
|
// }
|
|
752
752
|
```
|
|
753
753
|
|
|
754
|
-
The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value.
|
|
754
|
+
The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value.
|
|
755
|
+
|
|
756
|
+
**Behavior by type:**
|
|
757
|
+
|
|
758
|
+
| LHS type | RHS type | Result |
|
|
759
|
+
|----------|----------|--------|
|
|
760
|
+
| number | number | addition (`2 += 3` → `5`) |
|
|
761
|
+
| string | any | string concatenation (`"hello" += 3` → `"hello3"`) |
|
|
762
|
+
| array | array | array concatenation (`[1,2] += [3,4]` → `[1,2,3,4]`) |
|
|
763
|
+
| array | non-array | push single item (`[1,2] += 3` → `[1,2,3]`) |
|
|
764
|
+
| undefined/missing | any | direct assignment |
|
|
765
|
+
|
|
766
|
+
```TypeScript
|
|
767
|
+
const obj = {
|
|
768
|
+
tags: ['a', 'b'],
|
|
769
|
+
name: 'hello'
|
|
770
|
+
};
|
|
771
|
+
assignGingerly(obj, {
|
|
772
|
+
'?.tags +=': ['c', 'd'], // array concat: ['a', 'b', 'c', 'd']
|
|
773
|
+
'?.name +=': ' world' // string concat: 'hello world'
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Push a single item
|
|
777
|
+
assignGingerly(obj, { '?.tags +=': 'e' }); // ['a', 'b', 'c', 'd', 'e']
|
|
778
|
+
```
|
|
755
779
|
|
|
756
780
|
## Example 5 - Toggling boolean values and negating
|
|
757
781
|
|
|
@@ -4790,6 +4814,7 @@ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature }
|
|
|
4790
4814
|
| [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
4815
|
| [be-reflective](https://www.npmjs.com/package/be-reflective) | CSS custom state reflection from computed styles | [GitHub](https://github.com/bahrus/be-reflective) |
|
|
4792
4816
|
| [face-up](https://www.npmjs.com/package/face-up) | Form Associated Custom Element behavior via ElementInternals | [GitHub](https://github.com/bahrus/face-up) |
|
|
4817
|
+
| [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) |
|
|
4793
4818
|
| [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) |
|
|
4794
4819
|
|
|
4795
4820
|
</details>
|
package/assignFeatures.js
CHANGED
|
@@ -479,6 +479,86 @@ export function captureFeatureInitVals(instance) {
|
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
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
|
+
}
|
|
482
562
|
// =============================================================================
|
|
483
563
|
// PropertyBag — base class for nested feature containers
|
|
484
564
|
// =============================================================================
|
package/assignFeatures.ts
CHANGED
|
@@ -731,6 +731,114 @@ export function captureFeatureInitVals(instance: any): void {
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
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
|
+
|
|
734
842
|
// =============================================================================
|
|
735
843
|
// PropertyBag — base class for nested feature containers
|
|
736
844
|
// =============================================================================
|
package/assignGingerly.js
CHANGED
|
@@ -616,6 +616,11 @@ export function assignGingerly(target, source, options) {
|
|
|
616
616
|
if (!(lastKey in parent)) {
|
|
617
617
|
parent[lastKey] = value;
|
|
618
618
|
}
|
|
619
|
+
else if (Array.isArray(parent[lastKey])) {
|
|
620
|
+
parent[lastKey] = Array.isArray(value)
|
|
621
|
+
? [...parent[lastKey], ...value]
|
|
622
|
+
: [...parent[lastKey], value];
|
|
623
|
+
}
|
|
619
624
|
else {
|
|
620
625
|
parent[lastKey] += value;
|
|
621
626
|
}
|
|
@@ -625,6 +630,11 @@ export function assignGingerly(target, source, options) {
|
|
|
625
630
|
if (!(path in target)) {
|
|
626
631
|
target[path] = value;
|
|
627
632
|
}
|
|
633
|
+
else if (Array.isArray(target[path])) {
|
|
634
|
+
target[path] = Array.isArray(value)
|
|
635
|
+
? [...target[path], ...value]
|
|
636
|
+
: [...target[path], value];
|
|
637
|
+
}
|
|
628
638
|
else {
|
|
629
639
|
target[path] += value;
|
|
630
640
|
}
|
package/assignGingerly.ts
CHANGED
|
@@ -801,6 +801,10 @@ export function assignGingerly(
|
|
|
801
801
|
const parent = ensureNestedPath(target, pathParts);
|
|
802
802
|
if (!(lastKey in parent)) {
|
|
803
803
|
parent[lastKey] = value;
|
|
804
|
+
} else if (Array.isArray(parent[lastKey])) {
|
|
805
|
+
parent[lastKey] = Array.isArray(value)
|
|
806
|
+
? [...parent[lastKey], ...value]
|
|
807
|
+
: [...parent[lastKey], value];
|
|
804
808
|
} else {
|
|
805
809
|
parent[lastKey] += value;
|
|
806
810
|
}
|
|
@@ -808,6 +812,10 @@ export function assignGingerly(
|
|
|
808
812
|
// Plain key - direct operation on target
|
|
809
813
|
if (!(path in target)) {
|
|
810
814
|
target[path] = value;
|
|
815
|
+
} else if (Array.isArray(target[path])) {
|
|
816
|
+
target[path] = Array.isArray(value)
|
|
817
|
+
? [...target[path], ...value]
|
|
818
|
+
: [...target[path], value];
|
|
811
819
|
} else {
|
|
812
820
|
target[path] += value;
|
|
813
821
|
}
|
package/assignTentatively.js
CHANGED
|
@@ -124,7 +124,14 @@ export function assignTentatively(target, source, options) {
|
|
|
124
124
|
if (!(fullPath in reversal)) {
|
|
125
125
|
reversal[fullPath] = parent[lastKey];
|
|
126
126
|
}
|
|
127
|
-
parent[lastKey]
|
|
127
|
+
if (Array.isArray(parent[lastKey])) {
|
|
128
|
+
parent[lastKey] = Array.isArray(value)
|
|
129
|
+
? [...parent[lastKey], ...value]
|
|
130
|
+
: [...parent[lastKey], value];
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
parent[lastKey] += value;
|
|
134
|
+
}
|
|
128
135
|
}
|
|
129
136
|
else {
|
|
130
137
|
// Property doesn't exist, create it with the value
|
|
@@ -137,7 +144,14 @@ export function assignTentatively(target, source, options) {
|
|
|
137
144
|
if (!(path in reversal)) {
|
|
138
145
|
reversal[path] = target[path];
|
|
139
146
|
}
|
|
140
|
-
target[path]
|
|
147
|
+
if (Array.isArray(target[path])) {
|
|
148
|
+
target[path] = Array.isArray(value)
|
|
149
|
+
? [...target[path], ...value]
|
|
150
|
+
: [...target[path], value];
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
target[path] += value;
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
else {
|
|
143
157
|
target[path] = value;
|
package/assignTentatively.ts
CHANGED
|
@@ -152,7 +152,13 @@ export function assignTentatively(
|
|
|
152
152
|
if (!(fullPath in reversal)) {
|
|
153
153
|
reversal[fullPath] = parent[lastKey];
|
|
154
154
|
}
|
|
155
|
-
parent[lastKey]
|
|
155
|
+
if (Array.isArray(parent[lastKey])) {
|
|
156
|
+
parent[lastKey] = Array.isArray(value)
|
|
157
|
+
? [...parent[lastKey], ...value]
|
|
158
|
+
: [...parent[lastKey], value];
|
|
159
|
+
} else {
|
|
160
|
+
parent[lastKey] += value;
|
|
161
|
+
}
|
|
156
162
|
} else {
|
|
157
163
|
// Property doesn't exist, create it with the value
|
|
158
164
|
parent[lastKey] = value;
|
|
@@ -163,7 +169,13 @@ export function assignTentatively(
|
|
|
163
169
|
if (!(path in reversal)) {
|
|
164
170
|
reversal[path] = target[path];
|
|
165
171
|
}
|
|
166
|
-
target[path]
|
|
172
|
+
if (Array.isArray(target[path])) {
|
|
173
|
+
target[path] = Array.isArray(value)
|
|
174
|
+
? [...target[path], ...value]
|
|
175
|
+
: [...target[path], value];
|
|
176
|
+
} else {
|
|
177
|
+
target[path] += value;
|
|
178
|
+
}
|
|
167
179
|
} else {
|
|
168
180
|
target[path] = value;
|
|
169
181
|
}
|
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.
|
|
3
|
+
"version": "0.0.45",
|
|
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.
|
|
92
|
+
"@types/node": "25.8.0",
|
|
93
93
|
"typescript": "6.0.3"
|
|
94
94
|
}
|
|
95
95
|
}
|