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
@@ -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/core/Util.mjs
CHANGED
@@ -1,10 +1,7 @@
|
|
1
|
-
import Base from './Base.mjs';
|
2
|
-
|
3
1
|
/**
|
4
2
|
* @class Neo.core.Util
|
5
|
-
* @extends Neo.core.Base
|
6
3
|
*/
|
7
|
-
class Util
|
4
|
+
class Util {
|
8
5
|
/**
|
9
6
|
* A regex to remove camel case syntax
|
10
7
|
* @member {RegExp} decamelRegEx=/([a-z])([A-Z])/g
|
@@ -13,19 +10,6 @@ class Util extends Base {
|
|
13
10
|
*/
|
14
11
|
static decamelRegEx = /([a-z])([A-Z])/g
|
15
12
|
|
16
|
-
static config = {
|
17
|
-
/**
|
18
|
-
* @member {String} className='Neo.core.Util'
|
19
|
-
* @protected
|
20
|
-
*/
|
21
|
-
className: 'Neo.core.Util',
|
22
|
-
/**
|
23
|
-
* @member {String} ntype='core-util'
|
24
|
-
* @protected
|
25
|
-
*/
|
26
|
-
ntype: 'core-util'
|
27
|
-
}
|
28
|
-
|
29
13
|
/**
|
30
14
|
* @param {Object} scope
|
31
15
|
* @param {String[]} values
|
@@ -229,7 +213,8 @@ class Util extends Base {
|
|
229
213
|
}
|
230
214
|
}
|
231
215
|
|
232
|
-
|
216
|
+
const ns = Neo.ns('Neo.core', true);
|
217
|
+
ns.Util = Util;
|
233
218
|
|
234
219
|
// aliases
|
235
220
|
Neo.applyFromNs(Neo, Util, {
|
@@ -81,7 +81,7 @@ class RecordFactory extends Base {
|
|
81
81
|
return this[dataSymbol][fieldName]
|
82
82
|
},
|
83
83
|
set(value) {
|
84
|
-
|
84
|
+
this.notifyChange({
|
85
85
|
fields: {[fieldPath]: instance.parseRecordValue({record: this, field, value})},
|
86
86
|
model,
|
87
87
|
record: this
|
@@ -193,6 +193,25 @@ class RecordFactory extends Base {
|
|
193
193
|
return null
|
194
194
|
}
|
195
195
|
|
196
|
+
/**
|
197
|
+
* The single source of truth for record field changes.
|
198
|
+
* Executes instance.setRecordFields(), and can get used via:
|
199
|
+
* - Neo.util.Function:createSequence()
|
200
|
+
* - Neo.util.Function:intercept(),
|
201
|
+
* to "listen" to field changes
|
202
|
+
* @param {Object} data
|
203
|
+
* @param {Object} data.fields
|
204
|
+
* @param {Neo.data.Model} data.model
|
205
|
+
* @param {Object} data.record
|
206
|
+
* @param {Boolean} silent=false
|
207
|
+
* @returns {Object}
|
208
|
+
*/
|
209
|
+
notifyChange(data, silent=false) {
|
210
|
+
const param = {...data, silent}
|
211
|
+
instance.setRecordFields(param);
|
212
|
+
return param
|
213
|
+
}
|
214
|
+
|
196
215
|
/**
|
197
216
|
* Bulk-update multiple record fields at once
|
198
217
|
* @param {Object} fields
|
@@ -207,7 +226,7 @@ class RecordFactory extends Base {
|
|
207
226
|
* @param {Object} fields
|
208
227
|
*/
|
209
228
|
set(fields) {
|
210
|
-
|
229
|
+
this.notifyChange({fields, model, record: this})
|
211
230
|
}
|
212
231
|
|
213
232
|
/**
|
@@ -225,7 +244,7 @@ class RecordFactory extends Base {
|
|
225
244
|
* @param {Object} fields
|
226
245
|
*/
|
227
246
|
setSilent(fields) {
|
228
|
-
|
247
|
+
this.notifyChange({fields, model, record: this}, true)
|
229
248
|
}
|
230
249
|
|
231
250
|
/**
|
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
|
})
|