neo.mjs 10.0.0-beta.4 → 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 +2 -2
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- 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/datahandling/StateProviders.md +1 -0
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/tree.json +1 -0
- package/package.json +2 -2
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +226 -78
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +7 -2
- package/src/component/Base.mjs +67 -46
- package/src/container/Base.mjs +28 -24
- package/src/core/Base.mjs +138 -19
- package/src/core/Config.mjs +123 -32
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- 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/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/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
- 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/src/Neo.mjs
CHANGED
@@ -1,18 +1,46 @@
|
|
1
|
-
import DefaultConfig
|
2
|
-
import {isDescriptor}
|
1
|
+
import DefaultConfig from './DefaultConfig.mjs';
|
2
|
+
import {isDescriptor} from './core/ConfigSymbols.mjs';
|
3
3
|
|
4
4
|
const
|
5
5
|
camelRegex = /-./g,
|
6
6
|
configSymbol = Symbol.for('configSymbol'),
|
7
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
|
+
},
|
8
36
|
typeDetector = {
|
9
37
|
function: item => {
|
10
|
-
if (item.prototype?.constructor
|
38
|
+
if (item.prototype?.constructor?.isClass) {
|
11
39
|
return 'NeoClass'
|
12
40
|
}
|
13
41
|
},
|
14
42
|
object: item => {
|
15
|
-
if (item.constructor
|
43
|
+
if (item.constructor?.isClass && item instanceof Neo.core.Base) {
|
16
44
|
return 'NeoInstance'
|
17
45
|
}
|
18
46
|
}
|
@@ -23,7 +51,6 @@ const
|
|
23
51
|
* @module Neo
|
24
52
|
* @singleton
|
25
53
|
* @borrows Neo.core.Util.bindMethods as bindMethods
|
26
|
-
* @borrows Neo.core.Util.capitalize as capitalize
|
27
54
|
* @borrows Neo.core.Util.createStyleObject as createStyleObject
|
28
55
|
* @borrows Neo.core.Util.createStyles as createStyles
|
29
56
|
* @borrows Neo.core.Util.decamel as decamel
|
@@ -178,25 +205,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
178
205
|
* @returns {Object|Array|*} the cloned input
|
179
206
|
*/
|
180
207
|
clone(obj, deep=false, ignoreNeoInstances=false) {
|
181
|
-
|
182
|
-
|
183
|
-
return {
|
184
|
-
Array : () => !deep ? [...obj] : [...obj.map(val => Neo.clone(val, deep, ignoreNeoInstances))],
|
185
|
-
Date : () => new Date(obj.valueOf()),
|
186
|
-
Map : () => new Map(obj), // shallow copy
|
187
|
-
NeoInstance: () => ignoreNeoInstances ? obj : this.cloneNeoInstance(obj),
|
188
|
-
Set : () => new Set(obj),
|
189
|
-
|
190
|
-
Object: () => {
|
191
|
-
out = {};
|
192
|
-
|
193
|
-
Object.entries(obj).forEach(([key, value]) => {
|
194
|
-
out[key] = !deep ? value : Neo.clone(value, deep, ignoreNeoInstances)
|
195
|
-
});
|
196
|
-
|
197
|
-
return out
|
198
|
-
}
|
199
|
-
}[Neo.typeOf(obj)]?.() || obj
|
208
|
+
return cloneMap[Neo.typeOf(obj)]?.(obj, deep, ignoreNeoInstances) || obj
|
200
209
|
},
|
201
210
|
|
202
211
|
/**
|
@@ -264,7 +273,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
264
273
|
return null
|
265
274
|
}
|
266
275
|
|
267
|
-
className = config.className || config.module.prototype.className
|
276
|
+
className = config.className || config.module.prototype.className
|
268
277
|
}
|
269
278
|
|
270
279
|
if (!exists(className)) {
|
@@ -325,6 +334,10 @@ Neo = globalThis.Neo = Object.assign({
|
|
325
334
|
return Neo.merge(Neo.merge(target, defaults), source)
|
326
335
|
}
|
327
336
|
|
337
|
+
if (!target) {
|
338
|
+
return source
|
339
|
+
}
|
340
|
+
|
328
341
|
for (const key in source) {
|
329
342
|
const value = source[key];
|
330
343
|
|
@@ -338,6 +351,35 @@ Neo = globalThis.Neo = Object.assign({
|
|
338
351
|
return target
|
339
352
|
},
|
340
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
|
+
|
341
383
|
/**
|
342
384
|
* Maps a className string into a given or global namespace
|
343
385
|
* @example
|
@@ -461,13 +503,14 @@ Neo = globalThis.Neo = Object.assign({
|
|
461
503
|
* @returns {T}
|
462
504
|
*/
|
463
505
|
setupClass(cls) {
|
464
|
-
let
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
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;
|
471
514
|
|
472
515
|
/*
|
473
516
|
* If the namespace already exists, directly return it.
|
@@ -481,12 +524,16 @@ Neo = globalThis.Neo = Object.assign({
|
|
481
524
|
return ns
|
482
525
|
}
|
483
526
|
|
527
|
+
// Traverse the prototype chain to collect inherited configs and descriptors
|
484
528
|
while (proto.__proto__) {
|
485
529
|
ctor = proto.constructor;
|
486
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.
|
487
533
|
if (Object.hasOwn(ctor, 'classConfigApplied')) {
|
488
|
-
|
489
|
-
|
534
|
+
baseConfig = Neo.clone(ctor.config, true);
|
535
|
+
baseConfigDescriptors = Neo.clone(ctor.configDescriptors, true);
|
536
|
+
ntypeChain = [...ctor.ntypeChain];
|
490
537
|
break
|
491
538
|
}
|
492
539
|
|
@@ -494,29 +541,43 @@ Neo = globalThis.Neo = Object.assign({
|
|
494
541
|
proto = proto.__proto__
|
495
542
|
}
|
496
543
|
|
497
|
-
|
544
|
+
// Initialize accumulated config and descriptors
|
545
|
+
config = baseConfig || {};
|
546
|
+
configDescriptors = baseConfigDescriptors || {};
|
498
547
|
|
548
|
+
// Process each class in the prototype chain (from top to bottom)
|
499
549
|
protos.forEach(element => {
|
500
550
|
let mixins;
|
501
551
|
|
502
552
|
ctor = element.constructor;
|
503
|
-
|
504
|
-
cfg = ctor.config || {};
|
553
|
+
cfg = ctor.config || {};
|
505
554
|
|
506
555
|
if (Neo.overwrites) {
|
507
556
|
ctor.applyOverwrites?.(cfg)
|
508
557
|
}
|
509
558
|
|
559
|
+
// Process each config property defined in the current class's static config
|
510
560
|
Object.entries(cfg).forEach(([key, value]) => {
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
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
|
516
571
|
}
|
517
572
|
|
518
|
-
//
|
519
|
-
|
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.
|
520
581
|
else if (!Neo.hasPropertySetter(element, key)) {
|
521
582
|
Object.defineProperty(element, key, {
|
522
583
|
enumerable: true,
|
@@ -526,6 +587,17 @@ Neo = globalThis.Neo = Object.assign({
|
|
526
587
|
}
|
527
588
|
});
|
528
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
|
529
601
|
if (Object.hasOwn(cfg, 'ntype')) {
|
530
602
|
ntype = cfg.ntype;
|
531
603
|
|
@@ -540,6 +612,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
540
612
|
ntypeMap[ntype] = cfg.className
|
541
613
|
}
|
542
614
|
|
615
|
+
// Process mixins
|
543
616
|
mixins = Object.hasOwn(config, 'mixins') && config.mixins || [];
|
544
617
|
|
545
618
|
if (ctor.observable) {
|
@@ -551,7 +624,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
551
624
|
}
|
552
625
|
|
553
626
|
if (mixins.length > 0) {
|
554
|
-
applyMixins(ctor, mixins);
|
627
|
+
applyMixins(ctor, mixins, cfg);
|
555
628
|
|
556
629
|
if (Neo.ns('Neo.core.Observable', false, ctor.prototype.mixins)) {
|
557
630
|
ctor.observable = true
|
@@ -561,29 +634,45 @@ Neo = globalThis.Neo = Object.assign({
|
|
561
634
|
delete cfg.mixins;
|
562
635
|
delete config.mixins;
|
563
636
|
|
564
|
-
|
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];
|
565
641
|
|
642
|
+
if (descriptor?.merge) {
|
643
|
+
config[key] = Neo.mergeConfig(config[key], value, descriptor.merge)
|
644
|
+
} else {
|
645
|
+
config[key] = value
|
646
|
+
}
|
647
|
+
});
|
648
|
+
|
649
|
+
// Assign final processed config and descriptors to the class constructor
|
566
650
|
Object.assign(ctor, {
|
567
651
|
classConfigApplied: true,
|
568
|
-
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
|
569
654
|
isClass : true,
|
570
655
|
ntypeChain
|
571
656
|
});
|
572
657
|
|
658
|
+
// Apply to global namespace if not a singleton
|
573
659
|
!config.singleton && this.applyToGlobalNs(cls)
|
574
660
|
});
|
575
661
|
|
576
662
|
proto = cls.prototype || cls;
|
577
663
|
|
664
|
+
// Add is<Ntype> flags to the prototype
|
578
665
|
ntypeChain.forEach(ntype => {
|
579
666
|
proto[`is${Neo.capitalize(Neo.camel(ntype))}`] = true
|
580
667
|
});
|
581
668
|
|
669
|
+
// If it's a singleton, create and apply the instance to the global namespace
|
582
670
|
if (proto.singleton) {
|
583
671
|
cls = Neo.create(cls);
|
584
672
|
Neo.applyToGlobalNs(cls)
|
585
673
|
}
|
586
674
|
|
675
|
+
// Add class hierarchy information to the manager or a temporary map
|
587
676
|
hierarchyInfo = {
|
588
677
|
className : proto.className,
|
589
678
|
module : cls,
|
@@ -610,7 +699,7 @@ Neo = globalThis.Neo = Object.assign({
|
|
610
699
|
return null
|
611
700
|
}
|
612
701
|
|
613
|
-
return typeDetector[typeof item]?.(item) || item.constructor
|
702
|
+
return typeDetector[typeof item]?.(item) || item.constructor?.name
|
614
703
|
}
|
615
704
|
}, Neo);
|
616
705
|
|
@@ -624,6 +713,7 @@ const ignoreMixin = [
|
|
624
713
|
'classConfigApplied',
|
625
714
|
'className',
|
626
715
|
'constructor',
|
716
|
+
'id',
|
627
717
|
'isClass',
|
628
718
|
'mixin',
|
629
719
|
'ntype',
|
@@ -636,9 +726,10 @@ const ignoreMixin = [
|
|
636
726
|
/**
|
637
727
|
* @param {Neo.core.Base} cls
|
638
728
|
* @param {Array} mixins
|
729
|
+
* @param {Object} classConfig
|
639
730
|
* @private
|
640
731
|
*/
|
641
|
-
function applyMixins(cls, mixins) {
|
732
|
+
function applyMixins(cls, mixins, classConfig) {
|
642
733
|
if (!Array.isArray(mixins)) {
|
643
734
|
mixins = [mixins];
|
644
735
|
}
|
@@ -660,12 +751,12 @@ function applyMixins(cls, mixins) {
|
|
660
751
|
}
|
661
752
|
|
662
753
|
mixinCls = Neo.ns(mixin);
|
663
|
-
mixinProto = mixinCls.prototype
|
754
|
+
mixinProto = mixinCls.prototype
|
664
755
|
}
|
665
756
|
|
666
757
|
mixinProto.className.split('.').reduce(mixReduce(mixinCls), mixinClasses);
|
667
758
|
|
668
|
-
Object.
|
759
|
+
Object.entries(Object.getOwnPropertyDescriptors(mixinProto)).forEach(mixinProperty(cls.prototype, mixinProto, classConfig))
|
669
760
|
}
|
670
761
|
|
671
762
|
cls.prototype.mixins = mixinClasses // todo: we should do a deep merge
|
@@ -695,19 +786,37 @@ function autoGenerateGetSet(proto, key) {
|
|
695
786
|
}
|
696
787
|
|
697
788
|
if (!Neo[getSetCache][key]) {
|
698
|
-
|
789
|
+
// Public Descriptor
|
790
|
+
Neo[getSetCache][key] = {
|
699
791
|
get() {
|
700
792
|
let me = this,
|
793
|
+
config = me.getConfig(key),
|
701
794
|
hasNewKey = Object.hasOwn(me[configSymbol], key),
|
702
795
|
newKey = me[configSymbol][key],
|
703
796
|
value = hasNewKey ? newKey : me[_key];
|
704
797
|
|
705
|
-
if (
|
706
|
-
|
707
|
-
|
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
|
+
}
|
708
815
|
}
|
709
|
-
}
|
710
|
-
|
816
|
+
}
|
817
|
+
// legacy behavior
|
818
|
+
else if (Array.isArray(value)) {
|
819
|
+
value = [...value];
|
711
820
|
}
|
712
821
|
|
713
822
|
if (hasNewKey) {
|
@@ -728,45 +837,71 @@ function autoGenerateGetSet(proto, key) {
|
|
728
837
|
const config = this.getConfig(key);
|
729
838
|
if (!config) return;
|
730
839
|
|
731
|
-
let me
|
732
|
-
oldValue
|
840
|
+
let me = this,
|
841
|
+
oldValue = config.get(), // Get the old value from the Config instance
|
842
|
+
{EffectBatchManager} = Neo.core,
|
843
|
+
isNewBatch = !EffectBatchManager?.isBatchActive();
|
844
|
+
|
845
|
+
// If a config change is not triggered via `core.Base#set()`, honor changes inside hooks.
|
846
|
+
isNewBatch && EffectBatchManager?.startBatch();
|
733
847
|
|
734
|
-
//
|
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.
|
735
851
|
delete me[configSymbol][key];
|
736
852
|
|
737
|
-
|
738
|
-
|
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;
|
739
860
|
}
|
740
861
|
|
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.
|
865
|
+
me[_key] = value;
|
866
|
+
|
741
867
|
if (typeof me[beforeSet] === 'function') {
|
742
868
|
value = me[beforeSet](value, oldValue);
|
743
869
|
|
744
870
|
// If they don't return a value, that means no change
|
745
871
|
if (value === undefined) {
|
872
|
+
// Restore the original value if the update is canceled.
|
873
|
+
me[_key] = oldValue;
|
874
|
+
isNewBatch && EffectBatchManager?.endBatch();
|
746
875
|
return
|
747
876
|
}
|
748
877
|
}
|
749
878
|
|
750
|
-
//
|
751
|
-
//
|
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.
|
752
887
|
if (config.set(value)) {
|
753
888
|
me[afterSet]?.(value, oldValue);
|
754
889
|
me.afterSetConfig?.(key, value, oldValue)
|
755
890
|
}
|
891
|
+
|
892
|
+
isNewBatch && EffectBatchManager?.endBatch()
|
756
893
|
}
|
757
894
|
};
|
758
895
|
|
759
|
-
|
896
|
+
// Private Descriptor
|
897
|
+
Neo[getSetCache][_key] = {
|
760
898
|
get() {
|
761
|
-
return this.getConfig(key)?.get()
|
899
|
+
return this.getConfig(key)?.get()
|
762
900
|
},
|
763
901
|
set(value) {
|
764
|
-
this.getConfig(key)?.setRaw(value)
|
902
|
+
this.getConfig(key)?.setRaw(value)
|
765
903
|
}
|
766
|
-
}
|
767
|
-
|
768
|
-
Neo[getSetCache][key] = publicDescriptor;
|
769
|
-
Neo[getSetCache][_key] = privateDescriptor;
|
904
|
+
}
|
770
905
|
}
|
771
906
|
|
772
907
|
Object.defineProperty(proto, key, Neo[getSetCache][key]);
|
@@ -791,9 +926,7 @@ function createArrayNs(create, current, prev) {
|
|
791
926
|
arrRoot = prev[arrDetails[0]]
|
792
927
|
}
|
793
928
|
|
794
|
-
if (!arrRoot)
|
795
|
-
return
|
796
|
-
}
|
929
|
+
if (!arrRoot) return;
|
797
930
|
|
798
931
|
for (; i < len; i++) {
|
799
932
|
arrItem = parseInt(arrDetails[i]);
|
@@ -827,12 +960,27 @@ function exists(className) {
|
|
827
960
|
/**
|
828
961
|
* @param {Neo.core.Base} proto
|
829
962
|
* @param {Neo.core.Base} mixinProto
|
963
|
+
* @param {Object} classConfig
|
830
964
|
* @returns {Function}
|
831
965
|
* @private
|
832
966
|
*/
|
833
|
-
function mixinProperty(proto, mixinProto) {
|
834
|
-
return function(key) {
|
835
|
-
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
|
+
|
836
984
|
return
|
837
985
|
}
|
838
986
|
|
@@ -879,7 +1027,7 @@ function parseArrayFromString(str) {
|
|
879
1027
|
)
|
880
1028
|
}
|
881
1029
|
|
882
|
-
Neo.config
|
1030
|
+
Neo.config ??= {};
|
883
1031
|
|
884
1032
|
Neo.assignDefaults(Neo.config, DefaultConfig);
|
885
1033
|
|