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,217 @@
|
|
1
|
+
import Neo from '../../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../../src/core/_export.mjs';
|
3
|
+
import {createHierarchicalDataProxy} from '../../../../src/state/createHierarchicalDataProxy.mjs';
|
4
|
+
import Effect from '../../../../src/core/Effect.mjs';
|
5
|
+
import EffectManager from '../../../../src/core/EffectManager.mjs';
|
6
|
+
import Config from '../../../../src/core/Config.mjs';
|
7
|
+
import Base from '../../../../src/core/Base.mjs';
|
8
|
+
|
9
|
+
// Mock StateProvider for testing purposes
|
10
|
+
class MockStateProvider extends Base {
|
11
|
+
static config = {
|
12
|
+
className: 'Mock.State.Provider',
|
13
|
+
data_: null
|
14
|
+
}
|
15
|
+
|
16
|
+
#dataConfigs = {};
|
17
|
+
|
18
|
+
construct(config) {
|
19
|
+
super.construct(config);
|
20
|
+
}
|
21
|
+
|
22
|
+
afterSetData(value, oldValue) {
|
23
|
+
console.log(value);
|
24
|
+
if (value) {
|
25
|
+
this.processDataObject(value);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
processDataObject(obj, path = '') {
|
30
|
+
Object.entries(obj).forEach(([key, value]) => {
|
31
|
+
const fullPath = path ? `${path}.${key}` : key;
|
32
|
+
if (Neo.typeOf(value) === 'Object') {
|
33
|
+
this.processDataObject(value, fullPath);
|
34
|
+
} else {
|
35
|
+
const clonedValue = Neo.isObject(value) ? Neo.clone(value, true) : value; // Deep clone objects
|
36
|
+
this.#dataConfigs[fullPath] = new Config(clonedValue);
|
37
|
+
}
|
38
|
+
});
|
39
|
+
}
|
40
|
+
|
41
|
+
getDataConfig(path) {
|
42
|
+
return this.#dataConfigs[path] || null;
|
43
|
+
}
|
44
|
+
|
45
|
+
getOwnerOfDataProperty(path) {
|
46
|
+
if (this.#dataConfigs[path]) {
|
47
|
+
return {owner: this, propertyName: path};
|
48
|
+
}
|
49
|
+
const parent = this.getParent();
|
50
|
+
if (parent) {
|
51
|
+
return parent.getOwnerOfDataProperty(path);
|
52
|
+
}
|
53
|
+
return null;
|
54
|
+
}
|
55
|
+
|
56
|
+
hasNestedDataStartingWith(path) {
|
57
|
+
const pathWithDot = `${path}.`;
|
58
|
+
if (Object.keys(this.#dataConfigs).some(key => key.startsWith(pathWithDot))) {
|
59
|
+
return true;
|
60
|
+
}
|
61
|
+
const parent = this.getParent();
|
62
|
+
return parent ? parent.hasNestedDataStartingWith(path) : false;
|
63
|
+
}
|
64
|
+
|
65
|
+
getParent() {
|
66
|
+
// For testing, we'll manually set a parent
|
67
|
+
return this._parent || null;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
Neo.setupClass(MockStateProvider);
|
71
|
+
|
72
|
+
StartTest(t => {
|
73
|
+
t.it('should resolve data from a single provider', t => {
|
74
|
+
const provider = Neo.create(MockStateProvider, {data: {name: 'Neo', version: 10}});
|
75
|
+
let effectRunCount = 0;
|
76
|
+
|
77
|
+
const effect = new Effect({
|
78
|
+
fn: () => {
|
79
|
+
effectRunCount++;
|
80
|
+
const proxy = createHierarchicalDataProxy(provider);
|
81
|
+
if (effectRunCount === 1) {
|
82
|
+
t.is(proxy.name, 'Neo', 'Should get name from proxy (initial)');
|
83
|
+
t.is(proxy.version, 10, 'Should get version from proxy (initial)');
|
84
|
+
} else if (effectRunCount === 2) {
|
85
|
+
t.is(proxy.name, 'Neo.mjs', 'Should get name from proxy (updated)');
|
86
|
+
t.is(proxy.version, 10, 'Should get version from proxy (unchanged)');
|
87
|
+
}
|
88
|
+
}
|
89
|
+
});
|
90
|
+
|
91
|
+
t.is(effectRunCount, 1, 'Effect should run once initially');
|
92
|
+
t.is(effect.dependencies.size, 2, 'Effect should track 2 dependencies');
|
93
|
+
|
94
|
+
provider.getDataConfig('name').set('Neo.mjs');
|
95
|
+
t.is(effectRunCount, 2, 'Effect should re-run when dependency changes');
|
96
|
+
|
97
|
+
effect.destroy();
|
98
|
+
provider.destroy();
|
99
|
+
});
|
100
|
+
|
101
|
+
t.it('should resolve nested data from a single provider', t => {
|
102
|
+
const provider = Neo.create(MockStateProvider, {data: {user: {firstName: 'John', lastName: 'Doe'}}});
|
103
|
+
let effectRunCount = 0;
|
104
|
+
|
105
|
+
const effect = new Effect({
|
106
|
+
fn: () => {
|
107
|
+
effectRunCount++;
|
108
|
+
const proxy = createHierarchicalDataProxy(provider);
|
109
|
+
if (effectRunCount === 1) {
|
110
|
+
t.is(proxy.user.firstName, 'John', 'Should get nested firstName (initial)');
|
111
|
+
t.is(proxy.user.lastName, 'Doe', 'Should get nested lastName (initial)');
|
112
|
+
} else if (effectRunCount === 2) {
|
113
|
+
t.is(proxy.user.firstName, 'Jane', 'Should get nested firstName (updated)');
|
114
|
+
t.is(proxy.user.lastName, 'Doe', 'Should get nested lastName (unchanged)');
|
115
|
+
}
|
116
|
+
}
|
117
|
+
});
|
118
|
+
|
119
|
+
t.is(effectRunCount, 1, 'Effect should run once initially');
|
120
|
+
t.is(effect.dependencies.size, 2, 'Effect should track 2 nested dependencies');
|
121
|
+
|
122
|
+
provider.getDataConfig('user.firstName').set('Jane');
|
123
|
+
t.is(effectRunCount, 2, 'Effect should re-run when nested dependency changes');
|
124
|
+
|
125
|
+
effect.destroy();
|
126
|
+
provider.destroy();
|
127
|
+
});
|
128
|
+
|
129
|
+
t.it('should resolve data from parent providers', t => {
|
130
|
+
const parentProvider = Neo.create(MockStateProvider, {data: {appTitle: 'My App', user: {firstName: 'Parent'}}});
|
131
|
+
const childProvider = Neo.create(MockStateProvider, {data: {user: {lastName: 'Child'}}});
|
132
|
+
childProvider._parent = parentProvider; // Manually set parent
|
133
|
+
|
134
|
+
let effectRunCount = 0;
|
135
|
+
|
136
|
+
const effect = new Effect({
|
137
|
+
fn: () => {
|
138
|
+
effectRunCount++;
|
139
|
+
const proxy = createHierarchicalDataProxy(childProvider);
|
140
|
+
if (effectRunCount === 1) {
|
141
|
+
t.is(proxy.appTitle, 'My App', 'Should get appTitle from parent (initial)');
|
142
|
+
t.is(proxy.user.firstName, 'Parent', 'Should get firstName from parent (initial)');
|
143
|
+
t.is(proxy.user.lastName, 'Child', 'Should get lastName from child (initial)');
|
144
|
+
} else if (effectRunCount === 2) {
|
145
|
+
t.is(proxy.appTitle, 'New App Title', 'Should get appTitle from parent (updated)');
|
146
|
+
t.is(proxy.user.firstName, 'Parent', 'Should get firstName from parent (unchanged)');
|
147
|
+
t.is(proxy.user.lastName, 'Child', 'Should get lastName from child (unchanged)');
|
148
|
+
} else if (effectRunCount === 3) {
|
149
|
+
t.is(proxy.appTitle, 'New App Title', 'Should get appTitle from parent (unchanged)');
|
150
|
+
t.is(proxy.user.firstName, 'Parent', 'Should get firstName from parent (unchanged)');
|
151
|
+
t.is(proxy.user.lastName, 'New Child', 'Should get lastName from child (updated)');
|
152
|
+
} else if (effectRunCount === 4) {
|
153
|
+
t.is(proxy.appTitle, 'New App Title', 'Should get appTitle from parent (unchanged)');
|
154
|
+
t.is(proxy.user.firstName, 'New Parent', 'Should get firstName from parent (updated)');
|
155
|
+
t.is(proxy.user.lastName, 'New Child', 'Should get lastName from child (unchanged)');
|
156
|
+
}
|
157
|
+
}
|
158
|
+
});
|
159
|
+
|
160
|
+
t.is(effectRunCount, 1, 'Effect should run once initially');
|
161
|
+
t.is(effect.dependencies.size, 3, 'Effect should track 3 dependencies across hierarchy');
|
162
|
+
|
163
|
+
parentProvider.getDataConfig('appTitle').set('New App Title');
|
164
|
+
t.is(effectRunCount, 2, 'Effect should re-run when parent dependency changes');
|
165
|
+
|
166
|
+
childProvider.getDataConfig('user.lastName').set('New Child');
|
167
|
+
t.is(effectRunCount, 3, 'Effect should re-run when child dependency changes');
|
168
|
+
|
169
|
+
parentProvider.getDataConfig('user.firstName').set('New Parent');
|
170
|
+
t.is(effectRunCount, 4, 'Effect should re-run when parent nested dependency changes');
|
171
|
+
|
172
|
+
effect.destroy();
|
173
|
+
parentProvider.destroy();
|
174
|
+
childProvider.destroy();
|
175
|
+
});
|
176
|
+
|
177
|
+
t.it('should handle properties that are not data or nested paths', t => {
|
178
|
+
const provider = Neo.create(MockStateProvider, {data: {foo: 'bar'}});
|
179
|
+
let effectRunCount = 0;
|
180
|
+
|
181
|
+
const effect = new Effect({
|
182
|
+
fn: () => {
|
183
|
+
effectRunCount++;
|
184
|
+
const proxy = createHierarchicalDataProxy(provider);
|
185
|
+
t.is(proxy.nonExistent, null, 'Should return null for non-existent property');
|
186
|
+
t.is(proxy.foo, 'bar', 'Should still get existing data');
|
187
|
+
}
|
188
|
+
});
|
189
|
+
|
190
|
+
t.is(effectRunCount, 1, 'Effect should run once initially');
|
191
|
+
t.is(effect.dependencies.size, 1, 'Effect should only track existing data dependencies');
|
192
|
+
|
193
|
+
effect.destroy();
|
194
|
+
provider.destroy();
|
195
|
+
});
|
196
|
+
|
197
|
+
t.it('should not track dependencies when no effect is active', t => {
|
198
|
+
const provider = Neo.create(MockStateProvider, {data: {test: 123}});
|
199
|
+
const proxy = createHierarchicalDataProxy(provider);
|
200
|
+
|
201
|
+
// No active effect
|
202
|
+
t.is(EffectManager.getActiveEffect(), null, 'No active effect');
|
203
|
+
|
204
|
+
// Access property without active effect
|
205
|
+
const value = proxy.test;
|
206
|
+
t.is(value, 123, 'Should get value correctly');
|
207
|
+
|
208
|
+
// Verify no dependencies were added to a non-existent effect
|
209
|
+
const mockEffect = { addDependency: t.fail }; // If this is called, test fails
|
210
|
+
EffectManager.push(mockEffect); // Temporarily push a mock effect
|
211
|
+
EffectManager.pop(); // Immediately pop it
|
212
|
+
|
213
|
+
t.pass('No error when accessing proxy without active effect');
|
214
|
+
|
215
|
+
provider.destroy();
|
216
|
+
});
|
217
|
+
});
|