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
@@ -0,0 +1,127 @@
|
|
1
|
+
import EffectManager from './EffectManager.mjs';
|
2
|
+
import EffectBatchManager from './EffectBatchManager.mjs';
|
3
|
+
import IdGenerator from './IdGenerator.mjs';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Creates a reactive effect that automatically tracks its dependencies and re-runs when any of them change.
|
7
|
+
* This is a lightweight, plain JavaScript class for performance.
|
8
|
+
* It serves as a core reactive primitive, enabling automatic and dynamic dependency tracking.
|
9
|
+
* @class Neo.core.Effect
|
10
|
+
*/
|
11
|
+
class Effect {
|
12
|
+
/**
|
13
|
+
* A Map containing Config instances as keys and their cleanup functions as values.
|
14
|
+
* @member {Map} dependencies=new Map()
|
15
|
+
* @protected
|
16
|
+
*/
|
17
|
+
dependencies = new Map()
|
18
|
+
/**
|
19
|
+
* The function to execute.
|
20
|
+
* @member {Function|null} _fn=null
|
21
|
+
*/
|
22
|
+
_fn = null
|
23
|
+
/**
|
24
|
+
* The unique identifier for this effect instance.
|
25
|
+
* @member {String|null}
|
26
|
+
*/
|
27
|
+
id = IdGenerator.getId('effect')
|
28
|
+
/**
|
29
|
+
* @member {Boolean}
|
30
|
+
* @protected
|
31
|
+
*/
|
32
|
+
isDestroyed = false
|
33
|
+
/**
|
34
|
+
* @member {Boolean}
|
35
|
+
* @protected
|
36
|
+
*/
|
37
|
+
isRunning = false
|
38
|
+
|
39
|
+
/**
|
40
|
+
* @member fn
|
41
|
+
*/
|
42
|
+
get fn() {
|
43
|
+
return this._fn
|
44
|
+
}
|
45
|
+
set fn(value) {
|
46
|
+
this._fn = value;
|
47
|
+
// Assigning a new function to `fn` automatically triggers a re-run.
|
48
|
+
// This ensures that the effect immediately re-evaluates its dependencies
|
49
|
+
// based on the new function's logic, clearing old dependencies and establishing new ones.
|
50
|
+
this.run()
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* @param {Object} config
|
55
|
+
* @param {Function} config.fn The function to execute for the effect.
|
56
|
+
*/
|
57
|
+
constructor({fn}) {
|
58
|
+
this.fn = fn
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Cleans up all subscriptions and destroys the effect.
|
63
|
+
*/
|
64
|
+
destroy() {
|
65
|
+
const me = this;
|
66
|
+
|
67
|
+
me.dependencies.forEach(cleanup => cleanup());
|
68
|
+
me.dependencies.clear();
|
69
|
+
me.isDestroyed = true
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Executes the effect function, tracking its dependencies.
|
74
|
+
* This is called automatically on creation and whenever a dependency changes.
|
75
|
+
* The dynamic re-tracking ensures the effect always reflects its current dependencies,
|
76
|
+
* even if the logic within `fn` changes conditionally.
|
77
|
+
* @protected
|
78
|
+
*/
|
79
|
+
run() {
|
80
|
+
const me = this;
|
81
|
+
|
82
|
+
if (me.isDestroyed || me.isRunning) return;
|
83
|
+
|
84
|
+
if (EffectBatchManager.isBatchActive()) {
|
85
|
+
EffectBatchManager.queueEffect(me);
|
86
|
+
return
|
87
|
+
}
|
88
|
+
|
89
|
+
me.isRunning = true;
|
90
|
+
|
91
|
+
me.dependencies.forEach(cleanup => cleanup());
|
92
|
+
me.dependencies.clear();
|
93
|
+
|
94
|
+
EffectManager.push(me);
|
95
|
+
|
96
|
+
try {
|
97
|
+
me.fn()
|
98
|
+
} finally {
|
99
|
+
EffectManager.pop();
|
100
|
+
me.isRunning = false;
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
/**
|
105
|
+
* Adds a `Neo.core.Config` instance as a dependency for this effect.
|
106
|
+
* @param {Neo.core.Config} config The config instance to subscribe to.
|
107
|
+
* @protected
|
108
|
+
*/
|
109
|
+
addDependency(config) {
|
110
|
+
const me = this;
|
111
|
+
|
112
|
+
// Only add if not already a dependency. Map uses strict equality (===) for object keys.
|
113
|
+
if (!me.dependencies.has(config)) {
|
114
|
+
const cleanup = config.subscribe({
|
115
|
+
id: me.id,
|
116
|
+
fn: me.run.bind(me)
|
117
|
+
});
|
118
|
+
|
119
|
+
me.dependencies.set(config, cleanup)
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
const ns = Neo.ns('Neo.core', true);
|
125
|
+
ns.Effect = Effect;
|
126
|
+
|
127
|
+
export default Effect;
|
@@ -0,0 +1,68 @@
|
|
1
|
+
/**
|
2
|
+
* A singleton manager responsible for batching `Neo.core.Effect` executions.
|
3
|
+
* This ensures that effects triggered by multiple config changes within a single
|
4
|
+
* synchronous operation (e.g., `Neo.core.Base#set()`) are executed only once
|
5
|
+
* per batch, after all changes have been applied.
|
6
|
+
* @class Neo.core.EffectBatchManager
|
7
|
+
* @singleton
|
8
|
+
*/
|
9
|
+
const EffectBatchManager = {
|
10
|
+
/**
|
11
|
+
* The current count of active batch operations.
|
12
|
+
* Incremented by `startBatch()`, decremented by `endBatch()`.
|
13
|
+
* @member {Number} batchCount=0
|
14
|
+
*/
|
15
|
+
batchCount: 0,
|
16
|
+
/**
|
17
|
+
* A Set of `Neo.core.Effect` instances that are pending execution within the current batch.
|
18
|
+
* @member {Set<Neo.core.Effect>} pendingEffects=new Set()
|
19
|
+
*/
|
20
|
+
pendingEffects: new Set(),
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Increments the batch counter. When `batchCount` is greater than 0,
|
24
|
+
* effects will be queued instead of running immediately.
|
25
|
+
*/
|
26
|
+
startBatch() {
|
27
|
+
this.batchCount++
|
28
|
+
},
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Decrements the batch counter. If `batchCount` reaches 0, all queued effects
|
32
|
+
* are executed and the `pendingEffects` Set is cleared.
|
33
|
+
*/
|
34
|
+
endBatch() {
|
35
|
+
this.batchCount--;
|
36
|
+
|
37
|
+
if (this.batchCount === 0) {
|
38
|
+
this.pendingEffects.forEach(effect => {
|
39
|
+
effect.run();
|
40
|
+
});
|
41
|
+
|
42
|
+
this.pendingEffects.clear()
|
43
|
+
}
|
44
|
+
},
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Checks if there is an active batch operation.
|
48
|
+
* @returns {Boolean}
|
49
|
+
*/
|
50
|
+
isBatchActive() {
|
51
|
+
return this.batchCount > 0
|
52
|
+
},
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Queues an effect for execution at the end of the current batch.
|
56
|
+
* If the effect is already queued, it will not be added again.
|
57
|
+
* @param {Neo.core.Effect} effect The effect to queue.
|
58
|
+
*/
|
59
|
+
queueEffect(effect) {
|
60
|
+
this.pendingEffects.add(effect)
|
61
|
+
}
|
62
|
+
};
|
63
|
+
|
64
|
+
// Assign to Neo namespace
|
65
|
+
const ns = Neo.ns('Neo.core', true);
|
66
|
+
ns.EffectBatchManager = EffectBatchManager;
|
67
|
+
|
68
|
+
export default EffectBatchManager;
|
@@ -0,0 +1,38 @@
|
|
1
|
+
/**
|
2
|
+
* A singleton manager to track the currently running effect.
|
3
|
+
* This allows reactive properties to know which effect to subscribe to.
|
4
|
+
* @class Neo.core.EffectManager
|
5
|
+
* @singleton
|
6
|
+
*/
|
7
|
+
const EffectManager = {
|
8
|
+
effectStack: [],
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Returns the effect currently at the top of the stack (i.e., the one currently running).
|
12
|
+
* @returns {Neo.core.Effect|null}
|
13
|
+
*/
|
14
|
+
getActiveEffect() {
|
15
|
+
return this.effectStack[this.effectStack.length - 1]
|
16
|
+
},
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Pops the current effect from the stack, returning to the previous effect (if any).
|
20
|
+
* @returns {Neo.core.Effect|null}
|
21
|
+
*/
|
22
|
+
pop() {
|
23
|
+
return this.effectStack.pop()
|
24
|
+
},
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Pushes an effect onto the stack, marking it as the currently running effect.
|
28
|
+
* @param {Neo.core.Effect} effect The effect to push.
|
29
|
+
*/
|
30
|
+
push(effect) {
|
31
|
+
this.effectStack.push(effect)
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
const ns = Neo.ns('Neo.core', true);
|
36
|
+
ns.EffectManager = EffectManager;
|
37
|
+
|
38
|
+
export default EffectManager;
|
package/src/grid/Container.mjs
CHANGED
@@ -255,10 +255,13 @@ class GridContainer extends BaseContainer {
|
|
255
255
|
* @protected
|
256
256
|
*/
|
257
257
|
async afterSetColumns(value, oldValue) {
|
258
|
-
|
259
|
-
|
258
|
+
let me = this,
|
259
|
+
{headerToolbar} = me;
|
260
260
|
|
261
|
-
|
261
|
+
// - If columns changed at run-time OR
|
262
|
+
// - In case the `header.Toolbar#createItems()` method has run before columns where available
|
263
|
+
if (oldValue?.getCount?.() > 0 || (value?.count && headerToolbar?.isConstructed)) {
|
264
|
+
headerToolbar?.createItems()
|
262
265
|
|
263
266
|
await me.timeout(50);
|
264
267
|
|
@@ -611,7 +614,8 @@ class GridContainer extends BaseContainer {
|
|
611
614
|
*/
|
612
615
|
removeSortingCss(dataField) {
|
613
616
|
this.headerToolbar?.items.forEach(column => {
|
614
|
-
if (column.dataField !== dataField) {
|
617
|
+
if (column.dataField !== dataField) {return;
|
618
|
+
console.log(column, dataField)
|
615
619
|
column.removeSortingCss()
|
616
620
|
}
|
617
621
|
})
|