neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5
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/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/MainContainerController.mjs +6 -6
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
- package/learn/guides/datahandling/Grids.md +621 -0
- package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
- package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
- package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
- package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
- package/learn/tree.json +64 -57
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +244 -88
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +35 -3
- package/src/component/Base.mjs +72 -61
- package/src/container/Base.mjs +28 -24
- package/src/controller/Base.mjs +87 -63
- package/src/core/Base.mjs +207 -33
- package/src/core/Compare.mjs +3 -13
- package/src/core/Config.mjs +230 -0
- package/src/core/ConfigSymbols.mjs +3 -0
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/core/Util.mjs +3 -18
- package/src/data/RecordFactory.mjs +22 -3
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/util/Function.mjs +52 -5
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/config/Basic.mjs +149 -0
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
- package/learn/guides/ExtendingNeoClasses.md +0 -331
- /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
- /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
- /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
- /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
- /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
- /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
- /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
- /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
- /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
- /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
- /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
- /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
- /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
- /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
- /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
package/src/Neo.mjs
CHANGED
@@ -1,17 +1,46 @@
|
|
1
|
-
import DefaultConfig
|
1
|
+
import DefaultConfig from './DefaultConfig.mjs';
|
2
|
+
import {isDescriptor} from './core/ConfigSymbols.mjs';
|
2
3
|
|
3
4
|
const
|
4
5
|
camelRegex = /-./g,
|
5
6
|
configSymbol = Symbol.for('configSymbol'),
|
6
7
|
getSetCache = Symbol('getSetCache'),
|
8
|
+
cloneMap = {
|
9
|
+
Array(obj, deep, ignoreNeoInstances) {
|
10
|
+
return !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))]
|
11
|
+
},
|
12
|
+
Date(obj) {
|
13
|
+
return new Date(obj.valueOf())
|
14
|
+
},
|
15
|
+
Map(obj) {
|
16
|
+
return new Map(obj) // shallow copy
|
17
|
+
},
|
18
|
+
NeoInstance(obj, ignoreNeoInstances) {
|
19
|
+
return ignoreNeoInstances ? obj : Neo.cloneNeoInstance(obj)
|
20
|
+
},
|
21
|
+
Set(obj) {
|
22
|
+
return new Set(obj)
|
23
|
+
},
|
24
|
+
Object(obj, deep, ignoreNeoInstances) {
|
25
|
+
const out = {};
|
26
|
+
|
27
|
+
// Use Reflect.ownKeys() to include symbol properties (e.g., for config descriptors)
|
28
|
+
Reflect.ownKeys(obj).forEach(key => {
|
29
|
+
const value = obj[key];
|
30
|
+
out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
|
31
|
+
});
|
32
|
+
|
33
|
+
return out
|
34
|
+
}
|
35
|
+
},
|
7
36
|
typeDetector = {
|
8
37
|
function: item => {
|
9
|
-
if (item.prototype?.constructor
|
38
|
+
if (item.prototype?.constructor?.isClass) {
|
10
39
|
return 'NeoClass'
|
11
40
|
}
|
12
41
|
},
|
13
42
|
object: item => {
|
14
|
-
if (item.constructor
|
43
|
+
if (item.constructor?.isClass && item instanceof Neo.core.Base) {
|
15
44
|
return 'NeoInstance'
|
16
45
|
}
|
17
46
|
}
|
@@ -22,7 +51,6 @@ const
|
|
22
51
|
* @module Neo
|
23
52
|
* @singleton
|
24
53
|
* @borrows Neo.core.Util.bindMethods as bindMethods
|
25
|
-
* @borrows Neo.core.Util.capitalize as capitalize
|
26
54
|
* @borrows Neo.core.Util.createStyleObject as createStyleObject
|
27
55
|
* @borrows Neo.core.Util.createStyles as createStyles
|
28
56
|
* @borrows Neo.core.Util.decamel as decamel
|
@@ -177,25 +205,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
177
205
|
* @returns {Object|Array|*} the cloned input
|
178
206
|
*/
|
179
207
|
clone(obj, deep=false, ignoreNeoInstances=false) {
|
180
|
-
|
181
|
-
|
182
|
-
return {
|
183
|
-
Array : () => !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))],
|
184
|
-
Date : () => new Date(obj.valueOf()),
|
185
|
-
Map : () => new Map(obj), // shallow copy
|
186
|
-
NeoInstance: () => ignoreNeoInstances ? obj : this.cloneNeoInstance(obj),
|
187
|
-
Set : () => new Set(obj),
|
188
|
-
|
189
|
-
Object: () => {
|
190
|
-
out = {};
|
191
|
-
|
192
|
-
Object.entries(obj).forEach(([key, value]) => {
|
193
|
-
out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
|
194
|
-
});
|
195
|
-
|
196
|
-
return out
|
197
|
-
}
|
198
|
-
}[Neo.typeOf(obj)]?.() || obj
|
208
|
+
return cloneMap[Neo.typeOf(obj)]?.(obj, deep, ignoreNeoInstances) || obj
|
199
209
|
},
|
200
210
|
|
201
211
|
/**
|
@@ -263,7 +273,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
263
273
|
return null
|
264
274
|
}
|
265
275
|
|
266
|
-
className = config.className || config.module.prototype.className
|
276
|
+
className = config.className || config.module.prototype.className
|
267
277
|
}
|
268
278
|
|
269
279
|
if (!exists(className)) {
|
@@ -324,6 +334,10 @@ Neo = globalThis.Neo = Object.assign({
|
|
324
334
|
return Neo.merge(Neo.merge(target, defaults), source)
|
325
335
|
}
|
326
336
|
|
337
|
+
if (!target) {
|
338
|
+
return source
|
339
|
+
}
|
340
|
+
|
327
341
|
for (const key in source) {
|
328
342
|
const value = source[key];
|
329
343
|
|
@@ -337,6 +351,35 @@ Neo = globalThis.Neo = Object.assign({
|
|
337
351
|
return target
|
338
352
|
},
|
339
353
|
|
354
|
+
/**
|
355
|
+
* Merges a new value into an existing config value based on a specified strategy.
|
356
|
+
* This method is used during instance creation to apply merge strategies defined in config descriptors.
|
357
|
+
* @param {any} defaultValue - The default value of the config (from static config).
|
358
|
+
* @param {any} instanceValue - The value provided during instance creation.
|
359
|
+
* @param {string|Function} strategy - The merge strategy: 'shallow', 'deep', 'replace', or a custom function.
|
360
|
+
* @returns {any} The merged value.
|
361
|
+
*/
|
362
|
+
mergeConfig(defaultValue, instanceValue, strategy) {
|
363
|
+
const
|
364
|
+
defaultValueType = Neo.typeOf(defaultValue),
|
365
|
+
instanceValueType = Neo.typeOf(instanceValue);
|
366
|
+
|
367
|
+
if (strategy === 'shallow') {
|
368
|
+
if (defaultValueType === 'Object' && instanceValueType === 'Object') {
|
369
|
+
return {...defaultValue, ...instanceValue}
|
370
|
+
}
|
371
|
+
} else if (strategy === 'deep') {
|
372
|
+
if (defaultValueType === 'Object' && instanceValueType === 'Object') {
|
373
|
+
return Neo.merge(Neo.clone(defaultValue, true), instanceValue)
|
374
|
+
}
|
375
|
+
} else if (typeof strategy === 'function') {
|
376
|
+
return strategy(defaultValue, instanceValue)
|
377
|
+
}
|
378
|
+
|
379
|
+
// Default to 'replace' or if strategy is not recognized
|
380
|
+
return instanceValue
|
381
|
+
},
|
382
|
+
|
340
383
|
/**
|
341
384
|
* Maps a className string into a given or global namespace
|
342
385
|
* @example
|
@@ -460,13 +503,14 @@ Neo = globalThis.Neo = Object.assign({
|
|
460
503
|
* @returns {T}
|
461
504
|
*/
|
462
505
|
setupClass(cls) {
|
463
|
-
let
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
506
|
+
let baseConfig = null,
|
507
|
+
baseConfigDescriptors = null,
|
508
|
+
ntypeChain = [],
|
509
|
+
{ntypeMap} = Neo,
|
510
|
+
proto = cls.prototype || cls,
|
511
|
+
ns = Neo.ns(proto.constructor.config.className, false),
|
512
|
+
protos = [],
|
513
|
+
cfg, config, configDescriptors, ctor, hierarchyInfo, ntype;
|
470
514
|
|
471
515
|
/*
|
472
516
|
* If the namespace already exists, directly return it.
|
@@ -480,12 +524,16 @@ Neo = globalThis.Neo = Object.assign({
|
|
480
524
|
return ns
|
481
525
|
}
|
482
526
|
|
527
|
+
// Traverse the prototype chain to collect inherited configs and descriptors
|
483
528
|
while (proto.__proto__) {
|
484
529
|
ctor = proto.constructor;
|
485
530
|
|
531
|
+
// If a class in the prototype chain has already had its config applied,
|
532
|
+
// we can use its pre-processed config and descriptors as a base.
|
486
533
|
if (Object.hasOwn(ctor, 'classConfigApplied')) {
|
487
|
-
|
488
|
-
|
534
|
+
baseConfig = Neo.clone(ctor.config, true);
|
535
|
+
baseConfigDescriptors = Neo.clone(ctor.configDescriptors, true);
|
536
|
+
ntypeChain = [...ctor.ntypeChain];
|
489
537
|
break
|
490
538
|
}
|
491
539
|
|
@@ -493,29 +541,43 @@ Neo = globalThis.Neo = Object.assign({
|
|
493
541
|
proto = proto.__proto__
|
494
542
|
}
|
495
543
|
|
496
|
-
|
544
|
+
// Initialize accumulated config and descriptors
|
545
|
+
config = baseConfig || {};
|
546
|
+
configDescriptors = baseConfigDescriptors || {};
|
497
547
|
|
548
|
+
// Process each class in the prototype chain (from top to bottom)
|
498
549
|
protos.forEach(element => {
|
499
550
|
let mixins;
|
500
551
|
|
501
552
|
ctor = element.constructor;
|
502
|
-
|
503
|
-
cfg = ctor.config || {};
|
553
|
+
cfg = ctor.config || {};
|
504
554
|
|
505
555
|
if (Neo.overwrites) {
|
506
556
|
ctor.applyOverwrites?.(cfg)
|
507
557
|
}
|
508
558
|
|
559
|
+
// Process each config property defined in the current class's static config
|
509
560
|
Object.entries(cfg).forEach(([key, value]) => {
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
561
|
+
const
|
562
|
+
isReactive = key.slice(-1) === '_',
|
563
|
+
baseKey = isReactive ? key.slice(0, -1) : key;
|
564
|
+
|
565
|
+
// 1. Handle descriptors: If the value is a descriptor object, store it.
|
566
|
+
// The 'value' property of the descriptor is then used as the actual config value.
|
567
|
+
if (Neo.isObject(value) && value[isDescriptor] === true) {
|
568
|
+
ctor.configDescriptors ??= {};
|
569
|
+
ctor.configDescriptors[baseKey] = Neo.clone(value, true); // Deep clone to prevent mutation
|
570
|
+
value = value.value // Use the descriptor's value as the config value
|
515
571
|
}
|
516
572
|
|
517
|
-
|
518
|
-
|
573
|
+
// 2. Handle reactive vs. non-reactive configs: Generate getters/setters for reactive configs.
|
574
|
+
if (isReactive) {
|
575
|
+
delete cfg[key]; // Remove original key with underscore
|
576
|
+
cfg[baseKey] = value; // Use the potentially modified value
|
577
|
+
autoGenerateGetSet(element, baseKey)
|
578
|
+
}
|
579
|
+
// This part handles non-reactive configs (including those that were descriptors)
|
580
|
+
// If no property setter exists, define it directly on the prototype.
|
519
581
|
else if (!Neo.hasPropertySetter(element, key)) {
|
520
582
|
Object.defineProperty(element, key, {
|
521
583
|
enumerable: true,
|
@@ -525,6 +587,17 @@ Neo = globalThis.Neo = Object.assign({
|
|
525
587
|
}
|
526
588
|
});
|
527
589
|
|
590
|
+
// Merge configDescriptors: Apply "first-defined wins" strategy.
|
591
|
+
// If a descriptor for a key already exists (from a parent class), it is not overwritten.
|
592
|
+
if (ctor.configDescriptors) {
|
593
|
+
for (const key in ctor.configDescriptors) {
|
594
|
+
if (!Object.hasOwn(configDescriptors, key)) {
|
595
|
+
configDescriptors[key] = Neo.clone(ctor.configDescriptors[key], true) // Deep clone for immutability
|
596
|
+
}
|
597
|
+
}
|
598
|
+
}
|
599
|
+
|
600
|
+
// Process ntype and ntypeChain
|
528
601
|
if (Object.hasOwn(cfg, 'ntype')) {
|
529
602
|
ntype = cfg.ntype;
|
530
603
|
|
@@ -539,6 +612,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
539
612
|
ntypeMap[ntype] = cfg.className
|
540
613
|
}
|
541
614
|
|
615
|
+
// Process mixins
|
542
616
|
mixins = Object.hasOwn(config, 'mixins') && config.mixins || [];
|
543
617
|
|
544
618
|
if (ctor.observable) {
|
@@ -550,7 +624,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
550
624
|
}
|
551
625
|
|
552
626
|
if (mixins.length > 0) {
|
553
|
-
applyMixins(ctor, mixins);
|
627
|
+
applyMixins(ctor, mixins, cfg);
|
554
628
|
|
555
629
|
if (Neo.ns('Neo.core.Observable', false, ctor.prototype.mixins)) {
|
556
630
|
ctor.observable = true
|
@@ -560,29 +634,45 @@ Neo = globalThis.Neo = Object.assign({
|
|
560
634
|
delete cfg.mixins;
|
561
635
|
delete config.mixins;
|
562
636
|
|
563
|
-
|
637
|
+
// Hierarchical merging of static config values based on descriptors.
|
638
|
+
// This ensures that values are merged (e.g., shallow/deep) instead of simply overwritten.
|
639
|
+
Object.entries(cfg).forEach(([key, value]) => {
|
640
|
+
const descriptor = configDescriptors[key];
|
641
|
+
|
642
|
+
if (descriptor?.merge) {
|
643
|
+
config[key] = Neo.mergeConfig(config[key], value, descriptor.merge)
|
644
|
+
} else {
|
645
|
+
config[key] = value
|
646
|
+
}
|
647
|
+
});
|
564
648
|
|
649
|
+
// Assign final processed config and descriptors to the class constructor
|
565
650
|
Object.assign(ctor, {
|
566
651
|
classConfigApplied: true,
|
567
|
-
config : Neo.clone(config,
|
652
|
+
config : Neo.clone(config, true), // Deep clone final config for immutability
|
653
|
+
configDescriptors : Neo.clone(configDescriptors, true), // Deep clone final descriptors for immutability
|
568
654
|
isClass : true,
|
569
655
|
ntypeChain
|
570
656
|
});
|
571
657
|
|
658
|
+
// Apply to global namespace if not a singleton
|
572
659
|
!config.singleton && this.applyToGlobalNs(cls)
|
573
660
|
});
|
574
661
|
|
575
662
|
proto = cls.prototype || cls;
|
576
663
|
|
664
|
+
// Add is<Ntype> flags to the prototype
|
577
665
|
ntypeChain.forEach(ntype => {
|
578
666
|
proto[`is${Neo.capitalize(Neo.camel(ntype))}`] = true
|
579
667
|
});
|
580
668
|
|
669
|
+
// If it's a singleton, create and apply the instance to the global namespace
|
581
670
|
if (proto.singleton) {
|
582
671
|
cls = Neo.create(cls);
|
583
672
|
Neo.applyToGlobalNs(cls)
|
584
673
|
}
|
585
674
|
|
675
|
+
// Add class hierarchy information to the manager or a temporary map
|
586
676
|
hierarchyInfo = {
|
587
677
|
className : proto.className,
|
588
678
|
module : cls,
|
@@ -609,7 +699,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
609
699
|
return null
|
610
700
|
}
|
611
701
|
|
612
|
-
return typeDetector[typeof item]?.(item) || item.constructor
|
702
|
+
return typeDetector[typeof item]?.(item) || item.constructor?.name
|
613
703
|
}
|
614
704
|
}, Neo);
|
615
705
|
|
@@ -623,6 +713,7 @@ const ignoreMixin = [
|
|
623
713
|
'classConfigApplied',
|
624
714
|
'className',
|
625
715
|
'constructor',
|
716
|
+
'id',
|
626
717
|
'isClass',
|
627
718
|
'mixin',
|
628
719
|
'ntype',
|
@@ -635,9 +726,10 @@ const ignoreMixin = [
|
|
635
726
|
/**
|
636
727
|
* @param {Neo.core.Base} cls
|
637
728
|
* @param {Array} mixins
|
729
|
+
* @param {Object} classConfig
|
638
730
|
* @private
|
639
731
|
*/
|
640
|
-
function applyMixins(cls, mixins) {
|
732
|
+
function applyMixins(cls, mixins, classConfig) {
|
641
733
|
if (!Array.isArray(mixins)) {
|
642
734
|
mixins = [mixins];
|
643
735
|
}
|
@@ -659,12 +751,12 @@ function applyMixins(cls, mixins) {
|
|
659
751
|
}
|
660
752
|
|
661
753
|
mixinCls = Neo.ns(mixin);
|
662
|
-
mixinProto = mixinCls.prototype
|
754
|
+
mixinProto = mixinCls.prototype
|
663
755
|
}
|
664
756
|
|
665
757
|
mixinProto.className.split('.').reduce(mixReduce(mixinCls), mixinClasses);
|
666
758
|
|
667
|
-
Object.
|
759
|
+
Object.entries(Object.getOwnPropertyDescriptors(mixinProto)).forEach(mixinProperty(cls.prototype, mixinProto, classConfig))
|
668
760
|
}
|
669
761
|
|
670
762
|
cls.prototype.mixins = mixinClasses // todo: we should do a deep merge
|
@@ -682,30 +774,54 @@ function autoGenerateGetSet(proto, key) {
|
|
682
774
|
throw('Config ' + key + '_ (' + proto.className + ') already has a set method, use beforeGet, beforeSet & afterSet instead')
|
683
775
|
}
|
684
776
|
|
777
|
+
const
|
778
|
+
_key = '_' + key,
|
779
|
+
uKey = key[0].toUpperCase() + key.slice(1),
|
780
|
+
beforeGet = 'beforeGet' + uKey,
|
781
|
+
beforeSet = 'beforeSet' + uKey,
|
782
|
+
afterSet = 'afterSet' + uKey;
|
783
|
+
|
685
784
|
if (!Neo[getSetCache]) {
|
686
785
|
Neo[getSetCache] = {}
|
687
786
|
}
|
688
787
|
|
689
788
|
if (!Neo[getSetCache][key]) {
|
789
|
+
// Public Descriptor
|
690
790
|
Neo[getSetCache][key] = {
|
691
791
|
get() {
|
692
792
|
let me = this,
|
693
|
-
|
793
|
+
config = me.getConfig(key),
|
694
794
|
hasNewKey = Object.hasOwn(me[configSymbol], key),
|
695
795
|
newKey = me[configSymbol][key],
|
696
|
-
value = hasNewKey ? newKey : me[
|
796
|
+
value = hasNewKey ? newKey : me[_key];
|
697
797
|
|
698
|
-
if (
|
699
|
-
|
700
|
-
|
798
|
+
if (value instanceof Date) {
|
799
|
+
value = new Date(value.valueOf());
|
800
|
+
}
|
801
|
+
// new, explicit opt-in path
|
802
|
+
else if (config.cloneOnGet) {
|
803
|
+
const {cloneOnGet} = config;
|
804
|
+
|
805
|
+
if (cloneOnGet === 'deep') {
|
806
|
+
value = Neo.clone(value, true, true);
|
807
|
+
} else if (cloneOnGet === 'shallow') {
|
808
|
+
const type = Neo.typeOf(value);
|
809
|
+
|
810
|
+
if (type === 'Array') {
|
811
|
+
value = [...value];
|
812
|
+
} else if (type === 'Object') {
|
813
|
+
value = {...value};
|
814
|
+
}
|
701
815
|
}
|
702
|
-
}
|
703
|
-
|
816
|
+
}
|
817
|
+
// legacy behavior
|
818
|
+
else if (Array.isArray(value)) {
|
819
|
+
value = [...value];
|
704
820
|
}
|
705
821
|
|
706
822
|
if (hasNewKey) {
|
707
|
-
me[key] = value;
|
708
|
-
value = me[
|
823
|
+
me[key] = value; // We do want to trigger the setter => beforeSet, afterSet
|
824
|
+
value = me[_key]; // Return the value parsed by the setter
|
709
825
|
delete me[configSymbol][key]
|
710
826
|
}
|
711
827
|
|
@@ -715,28 +831,37 @@ function autoGenerateGetSet(proto, key) {
|
|
715
831
|
|
716
832
|
return value
|
717
833
|
},
|
718
|
-
|
719
834
|
set(value) {
|
720
|
-
if (value === undefined)
|
721
|
-
return
|
722
|
-
}
|
835
|
+
if (value === undefined) return;
|
723
836
|
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
837
|
+
const config = this.getConfig(key);
|
838
|
+
if (!config) return;
|
839
|
+
|
840
|
+
let me = this,
|
841
|
+
oldValue = config.get(), // Get the old value from the Config instance
|
842
|
+
{EffectBatchManager} = Neo.core,
|
843
|
+
isNewBatch = !EffectBatchManager?.isBatchActive();
|
730
844
|
|
731
|
-
//
|
845
|
+
// If a config change is not triggered via `core.Base#set()`, honor changes inside hooks.
|
846
|
+
isNewBatch && EffectBatchManager?.startBatch();
|
847
|
+
|
848
|
+
// 1. Prevent infinite loops:
|
849
|
+
// Immediately remove the pending value from the configSymbol to prevent a getter from
|
850
|
+
// recursively re-triggering this setter.
|
732
851
|
delete me[configSymbol][key];
|
733
852
|
|
734
|
-
|
735
|
-
|
853
|
+
switch (config.clone) {
|
854
|
+
case 'deep':
|
855
|
+
value = Neo.clone(value, true, true);
|
856
|
+
break;
|
857
|
+
case 'shallow':
|
858
|
+
value = Neo.clone(value, false, true);
|
859
|
+
break;
|
736
860
|
}
|
737
861
|
|
738
|
-
//
|
739
|
-
//
|
862
|
+
// 2. Create a temporary state for beforeSet hooks:
|
863
|
+
// Set the new value directly on the private backing property. This allows any beforeSet
|
864
|
+
// hook to access the new value of this and other configs within the same `set()` call.
|
740
865
|
me[_key] = value;
|
741
866
|
|
742
867
|
if (typeof me[beforeSet] === 'function') {
|
@@ -744,25 +869,43 @@ function autoGenerateGetSet(proto, key) {
|
|
744
869
|
|
745
870
|
// If they don't return a value, that means no change
|
746
871
|
if (value === undefined) {
|
872
|
+
// Restore the original value if the update is canceled.
|
747
873
|
me[_key] = oldValue;
|
874
|
+
isNewBatch && EffectBatchManager?.endBatch();
|
748
875
|
return
|
749
876
|
}
|
750
|
-
|
751
|
-
me[_key] = value;
|
752
877
|
}
|
753
878
|
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
879
|
+
// 3. Restore state for change detection:
|
880
|
+
// Revert the private backing property to its original value. This is crucial for the
|
881
|
+
// `config.set()` method to correctly detect if the value has actually changed.
|
882
|
+
me[_key] = oldValue;
|
883
|
+
|
884
|
+
// 4. Finalize the change:
|
885
|
+
// The config.set() method performs the final check and, if the value changed,
|
886
|
+
// triggers afterSet hooks and notifies subscribers.
|
887
|
+
if (config.set(value)) {
|
758
888
|
me[afterSet]?.(value, oldValue);
|
759
889
|
me.afterSetConfig?.(key, value, oldValue)
|
760
890
|
}
|
891
|
+
|
892
|
+
isNewBatch && EffectBatchManager?.endBatch()
|
893
|
+
}
|
894
|
+
};
|
895
|
+
|
896
|
+
// Private Descriptor
|
897
|
+
Neo[getSetCache][_key] = {
|
898
|
+
get() {
|
899
|
+
return this.getConfig(key)?.get()
|
900
|
+
},
|
901
|
+
set(value) {
|
902
|
+
this.getConfig(key)?.setRaw(value)
|
761
903
|
}
|
762
904
|
}
|
763
905
|
}
|
764
906
|
|
765
|
-
Object.defineProperty(proto, key,
|
907
|
+
Object.defineProperty(proto, key, Neo[getSetCache][key]);
|
908
|
+
Object.defineProperty(proto, _key, Neo[getSetCache][_key])
|
766
909
|
}
|
767
910
|
|
768
911
|
/**
|
@@ -783,9 +926,7 @@ function createArrayNs(create, current, prev) {
|
|
783
926
|
arrRoot = prev[arrDetails[0]]
|
784
927
|
}
|
785
928
|
|
786
|
-
if (!arrRoot)
|
787
|
-
return
|
788
|
-
}
|
929
|
+
if (!arrRoot) return;
|
789
930
|
|
790
931
|
for (; i < len; i++) {
|
791
932
|
arrItem = parseInt(arrDetails[i]);
|
@@ -819,12 +960,27 @@ function exists(className) {
|
|
819
960
|
/**
|
820
961
|
* @param {Neo.core.Base} proto
|
821
962
|
* @param {Neo.core.Base} mixinProto
|
963
|
+
* @param {Object} classConfig
|
822
964
|
* @returns {Function}
|
823
965
|
* @private
|
824
966
|
*/
|
825
|
-
function mixinProperty(proto, mixinProto) {
|
826
|
-
return function(key) {
|
827
|
-
if (
|
967
|
+
function mixinProperty(proto, mixinProto, classConfig) {
|
968
|
+
return function([key, descriptor]) {
|
969
|
+
if (ignoreMixin.includes(key)) return;
|
970
|
+
|
971
|
+
// Mixins must not override existing class properties with a setter
|
972
|
+
if (Neo.hasPropertySetter(proto, key)) return;
|
973
|
+
|
974
|
+
// Reactive neo configs, or public class fields defined via get() AND set()
|
975
|
+
if (descriptor.get && descriptor.set) {
|
976
|
+
autoGenerateGetSet(proto, key);
|
977
|
+
|
978
|
+
const mixinClassConfig = mixinProto.constructor.config;
|
979
|
+
|
980
|
+
if (Object.hasOwn(mixinClassConfig, key)) {
|
981
|
+
classConfig[key] = mixinClassConfig[key];
|
982
|
+
}
|
983
|
+
|
828
984
|
return
|
829
985
|
}
|
830
986
|
|
@@ -871,7 +1027,7 @@ function parseArrayFromString(str) {
|
|
871
1027
|
)
|
872
1028
|
}
|
873
1029
|
|
874
|
-
Neo.config
|
1030
|
+
Neo.config ??= {};
|
875
1031
|
|
876
1032
|
Neo.assignDefaults(Neo.config, DefaultConfig);
|
877
1033
|
|