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,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
+ });