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.
Files changed (46) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +2 -2
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  5. package/examples/button/effect/MainContainer.mjs +207 -0
  6. package/examples/button/effect/app.mjs +6 -0
  7. package/examples/button/effect/index.html +11 -0
  8. package/examples/button/effect/neo-config.json +6 -0
  9. package/learn/guides/datahandling/StateProviders.md +1 -0
  10. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  11. package/learn/tree.json +1 -0
  12. package/package.json +2 -2
  13. package/src/DefaultConfig.mjs +2 -2
  14. package/src/Neo.mjs +226 -78
  15. package/src/button/Effect.mjs +435 -0
  16. package/src/collection/Base.mjs +7 -2
  17. package/src/component/Base.mjs +67 -46
  18. package/src/container/Base.mjs +28 -24
  19. package/src/core/Base.mjs +138 -19
  20. package/src/core/Config.mjs +123 -32
  21. package/src/core/Effect.mjs +127 -0
  22. package/src/core/EffectBatchManager.mjs +68 -0
  23. package/src/core/EffectManager.mjs +38 -0
  24. package/src/grid/Container.mjs +8 -4
  25. package/src/grid/column/Component.mjs +1 -1
  26. package/src/state/Provider.mjs +343 -452
  27. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  28. package/src/tab/header/EffectButton.mjs +75 -0
  29. package/src/vdom/Helper.mjs +9 -10
  30. package/src/vdom/VNode.mjs +1 -1
  31. package/src/worker/App.mjs +0 -5
  32. package/test/siesta/siesta.js +32 -0
  33. package/test/siesta/tests/CollectionBase.mjs +10 -10
  34. package/test/siesta/tests/VdomHelper.mjs +22 -59
  35. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  36. package/test/siesta/tests/{ReactiveConfigs.mjs → config/Basic.mjs} +58 -21
  37. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  38. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  39. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  40. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  41. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  42. package/test/siesta/tests/core/Effect.mjs +131 -0
  43. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  44. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  45. package/test/siesta/tests/state/Provider.mjs +537 -0
  46. 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;
@@ -255,10 +255,13 @@ class GridContainer extends BaseContainer {
255
255
  * @protected
256
256
  */
257
257
  async afterSetColumns(value, oldValue) {
258
- if (oldValue?.getCount?.() > 0) {
259
- let me = this;
258
+ let me = this,
259
+ {headerToolbar} = me;
260
260
 
261
- me.headerToolbar?.createItems()
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
  })
@@ -118,7 +118,7 @@ class Component extends Column {
118
118
  }
119
119
 
120
120
  if (me.useBindings) {
121
- body.getStateProvider()?.parseConfig(component)
121
+ body.getStateProvider()?.createBindings(component)
122
122
  }
123
123
 
124
124
  body.updateDepth = -1;