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,124 @@
1
+ import EffectManager from '../core/EffectManager.mjs';
2
+
3
+ /**
4
+ * Creates a nested Proxy that represents a level in the hierarchical data structure.
5
+ * @param {Neo.state.Provider} rootProvider The top-level provider to start searches from.
6
+ * @param {String} path The current path of this proxy level (e.g., 'user' for data.user).
7
+ * @returns {Proxy|null}
8
+ * @private
9
+ */
10
+ function createNestedProxy(rootProvider, path) {
11
+ // The target object for the proxy can be empty because all lookups are dynamic.
12
+ const target = {};
13
+
14
+ return new Proxy(target, {
15
+ /**
16
+ * The get trap for the proxy. This is where the magic happens.
17
+ * @param {Object} currentTarget The proxy's target object.
18
+ * @param {String|Symbol} property The name of the property being accessed.
19
+ * @returns {*} The value of the property or a new proxy for nested access.
20
+ */
21
+ get(currentTarget, property) {
22
+ // Handle internal properties that might be set directly on the proxy's target
23
+ // or are expected by the environment (like Siesta's __REFADR__).
24
+ if (typeof property === 'symbol' || property === '__REFADR__' || property === 'inspect' || property === 'then') {
25
+ return Reflect.get(currentTarget, property);
26
+ }
27
+
28
+ // Only allow string or number properties to proceed as data paths.
29
+ if (typeof property !== 'string' && typeof property !== 'number') {
30
+ return undefined; // For other non-string/non-number properties, return undefined.
31
+ }
32
+
33
+ const fullPath = path ? `${path}.${property}` : property;
34
+
35
+ // 1. Check if the full path corresponds to an actual data property.
36
+ const ownerDetails = rootProvider.getOwnerOfDataProperty(fullPath);
37
+
38
+ if (ownerDetails) {
39
+ const
40
+ {owner, propertyName} = ownerDetails,
41
+ config = owner.getDataConfig(propertyName);
42
+
43
+ if (config) {
44
+ const activeEffect = EffectManager.getActiveEffect();
45
+ if (activeEffect) {
46
+ activeEffect.addDependency(config);
47
+ }
48
+
49
+ const value = config.get();
50
+ // If the value is an object, return a new proxy for it to ensure nested accesses are also proxied.
51
+ if (Neo.typeOf(value) === 'Object') {
52
+ return createNestedProxy(rootProvider, fullPath)
53
+ }
54
+ return value;
55
+ }
56
+ }
57
+
58
+ // 2. If not a direct match, it might be a parent object of a nested property
59
+ // (e.g., accessing `user` when a `user.firstname` binding exists).
60
+ // In this case, we return another proxy for the next level down.
61
+ if (rootProvider.hasNestedDataStartingWith(fullPath)) {
62
+ return createNestedProxy(rootProvider, fullPath)
63
+ }
64
+
65
+ // 3. If it's neither a data property nor a path to one, it doesn't exist in the state.
66
+ return null
67
+ },
68
+
69
+ set(currentTarget, property, value) {
70
+ // Allow internal properties (like Symbols or specific strings) to be set directly on the target.
71
+ if (typeof property === 'symbol' || property === '__REFADR__') {
72
+ return Reflect.set(currentTarget, property, value);
73
+ }
74
+
75
+ const fullPath = path ? `${path}.${property}` : property;
76
+ const ownerDetails = rootProvider.getOwnerOfDataProperty(fullPath);
77
+
78
+ let targetProvider;
79
+ if (ownerDetails) {
80
+ targetProvider = ownerDetails.owner;
81
+ } else {
82
+ // If no owner is found, set it on the rootProvider (the one that created this proxy)
83
+ targetProvider = rootProvider;
84
+ }
85
+
86
+ targetProvider.setData(fullPath, value);
87
+ return true; // Indicate that the assignment was successful
88
+ },
89
+
90
+ ownKeys(currentTarget) {
91
+ return rootProvider.getTopLevelDataKeys(path);
92
+ },
93
+
94
+ getOwnPropertyDescriptor(currentTarget, property) {
95
+ const fullPath = path ? `${path}.${property}` : property;
96
+ const ownerDetails = rootProvider.getOwnerOfDataProperty(fullPath);
97
+
98
+ if (ownerDetails) {
99
+ const config = ownerDetails.owner.getDataConfig(ownerDetails.propertyName);
100
+ if (config) {
101
+ const value = config.get();
102
+ return {
103
+ value: Neo.isObject(value) ? createNestedProxy(rootProvider, fullPath) : value,
104
+ writable: true,
105
+ enumerable: true,
106
+ configurable: true,
107
+ };
108
+ }
109
+ }
110
+ return undefined; // Property not found
111
+ }
112
+ })
113
+ }
114
+
115
+ /**
116
+ * Creates a Proxy object that represents the merged, hierarchical data from a `state.Provider` chain.
117
+ * When a property is accessed through this proxy while an Effect is running, it automatically
118
+ * tracks the underlying core.Config instance as a dependency.
119
+ * @param {Neo.state.Provider} provider The starting state.Provider.
120
+ * @returns {Proxy}
121
+ */
122
+ export function createHierarchicalDataProxy(provider) {
123
+ return createNestedProxy(provider, '')
124
+ }
@@ -0,0 +1,75 @@
1
+ import EffectButton from '../../button/Effect.mjs';
2
+
3
+ /**
4
+ * @class Neo.tab.header.EffectButton
5
+ * @extends Neo.button.Effect
6
+ */
7
+ class EffectTabButton extends EffectButton {
8
+ static config = {
9
+ /**
10
+ * @member {String} className='Neo.tab.header.EffectButton'
11
+ * @protected
12
+ */
13
+ className: 'Neo.tab.header.EffectButton',
14
+ /**
15
+ * @member {String} ntype='tab-header-effect-button'
16
+ * @protected
17
+ */
18
+ ntype: 'tab-header-effect-button',
19
+ /**
20
+ * @member {String[]} baseCls=['neo-tab-header-button', 'neo-button']
21
+ */
22
+ baseCls: ['neo-tab-header-button', 'neo-button'],
23
+ /**
24
+ * Specify a role tag attribute for the vdom root.
25
+ * @member {String|null} role='tab'
26
+ */
27
+ role: 'tab',
28
+ /**
29
+ * @member {Boolean} useActiveTabIndicator_=true
30
+ */
31
+ useActiveTabIndicator_: true
32
+ }
33
+
34
+ /**
35
+ * Builds the top-level VDOM object.
36
+ * @returns {Object}
37
+ * @protected
38
+ */
39
+ getVdomConfig() {
40
+ let vdomConfig = super.getVdomConfig();
41
+
42
+ vdomConfig.role = this.role;
43
+
44
+ if (this.pressed) {
45
+ vdomConfig['aria-selected'] = true;
46
+ }
47
+
48
+ return vdomConfig;
49
+ }
50
+
51
+ /**
52
+ * Builds the array of child nodes (the 'cn' property).
53
+ * @returns {Object[]}
54
+ * @protected
55
+ */
56
+ getVdomChildren() {
57
+ let children = super.getVdomChildren();
58
+
59
+ children.push({
60
+ cls : ['neo-tab-button-indicator'],
61
+ removeDom: !this.useActiveTabIndicator
62
+ });
63
+
64
+ return children;
65
+ }
66
+
67
+ /**
68
+ * @param {Object} data
69
+ */
70
+ showRipple(data) {
71
+ !this.pressed && super.showRipple(data);
72
+ }
73
+ }
74
+
75
+ export default Neo.setupClass(EffectTabButton);
@@ -529,16 +529,15 @@ class Helper extends Base {
529
529
  * @protected
530
530
  */
531
531
  insertNode({deltas, index, oldVnodeMap, vnode, vnodeMap}) {
532
- let details = vnodeMap.get(vnode.id),
533
- {parentNode} = details,
534
- parentId = parentNode.id,
535
- me = this,
536
- movedNodes = me.findMovedNodes({oldVnodeMap, vnode, vnodeMap}),
537
- delta = {action: 'insertNode', parentId},
538
- hasLeadingTextChildren = false,
539
- physicalIndex = me.getPhysicalIndex(parentNode, index); // Processes the children of the *NEW* parent's VNode in the *current* state
540
-
541
- Object.assign(delta, {hasLeadingTextChildren, index: physicalIndex});
532
+ let details = vnodeMap.get(vnode.id),
533
+ {parentNode} = details,
534
+ parentId = parentNode.id,
535
+ me = this,
536
+ movedNodes = me.findMovedNodes({oldVnodeMap, vnode, vnodeMap}),
537
+ delta = {action: 'insertNode', parentId};
538
+
539
+ // Processes the children of the *NEW* parent's VNode in the *current* state
540
+ delta.index = me.getPhysicalIndex(parentNode, index);
542
541
 
543
542
  if (NeoConfig.useDomApiRenderer) {
544
543
  // For direct DOM API mounting, pass the pruned VNode tree
@@ -78,7 +78,7 @@ class VNode {
78
78
  attributes: config.attributes || {},
79
79
  className : normalizeClassName(config.className),
80
80
  nodeName : config.nodeName || 'div',
81
- style : config.style
81
+ style : config.style || {}
82
82
  });
83
83
 
84
84
  // Use vdom.html on your own risk, it is not fully XSS secure.
@@ -45,11 +45,6 @@ class App extends Base {
45
45
  singleton: true
46
46
  }
47
47
 
48
- /**
49
- * @member {Boolean} isUsingStateProviders=false
50
- * @protected
51
- */
52
- isUsingStateProviders = false
53
48
  /**
54
49
  * We are storing the params of insertThemeFiles() calls here, in case the method does get triggered
55
50
  * before the json theme structure got loaded.
@@ -17,8 +17,40 @@ project.configure({
17
17
  });
18
18
 
19
19
  project.plan(
20
+ {
21
+ group: 'neo',
22
+ items: [
23
+ 'tests/neo/MixinStaticConfig.mjs'
24
+ ]
25
+ },
20
26
  'tests/ClassConfigsAndFields.mjs',
21
27
  'tests/ClassSystem.mjs',
28
+ {
29
+ group: 'core',
30
+ items: [
31
+ 'tests/core/Effect.mjs'
32
+ ]
33
+ },
34
+ {
35
+ group: 'config',
36
+ items: [
37
+ 'tests/config/Basic.mjs',
38
+ 'tests/config/Hierarchy.mjs',
39
+ 'tests/config/MultiLevelHierarchy.mjs',
40
+ 'tests/config/CustomFunctions.mjs',
41
+ 'tests/config/AfterSetConfig.mjs',
42
+ 'tests/config/MemoryLeak.mjs',
43
+ 'tests/config/CircularDependencies.mjs',
44
+ 'tests/core/EffectBatching.mjs'
45
+ ]
46
+ },
47
+ {
48
+ group: 'state',
49
+ items: [
50
+ 'tests/state/createHierarchicalDataProxy.mjs',
51
+ 'tests/state/Provider.mjs'
52
+ ]
53
+ },
22
54
  'tests/CollectionBase.mjs',
23
55
  'tests/ManagerInstance.mjs',
24
56
  'tests/Rectangle.mjs',
@@ -28,20 +28,20 @@ StartTest(t => {
28
28
  ]
29
29
  });
30
30
 
31
- t.isStrict(collection.getCount(), 6, 'Collection has 6 items');
31
+ t.isStrict(collection.count, 6, 'Collection has 6 items');
32
32
  t.isStrict(collection.map.size, 6, 'map has 6 items');
33
33
  });
34
34
 
35
35
  t.it('Modify collection items', t => {
36
36
  collection.add({country: 'Germany', firstname: 'Bastian', githubId: 'bhaustein', lastname: 'Haustein'});
37
37
 
38
- t.isStrict(collection.getCount(), 7, 'Collection has 7 items');
38
+ t.isStrict(collection.count, 7, 'Collection has 7 items');
39
39
  t.isStrict(collection.map.size, 7, 'map has 7 items');
40
40
 
41
41
 
42
42
  collection.remove('bhaustein');
43
43
 
44
- t.isStrict(collection.getCount(), 6, 'Collection has 6 items');
44
+ t.isStrict(collection.count, 6, 'Collection has 6 items');
45
45
  t.isStrict(collection.map.size, 6, 'map has 6 items');
46
46
 
47
47
  collection.insert(1, [
@@ -49,12 +49,12 @@ StartTest(t => {
49
49
  {country: 'Argentina', firstname: 'Max', githubId: 'elmasse', lastname: 'Fierro'}
50
50
  ]);
51
51
 
52
- t.isStrict(collection.getCount(), 8, 'Collection has 8 items');
52
+ t.isStrict(collection.count, 8, 'Collection has 8 items');
53
53
  t.isStrict(collection.map.size, 8, 'map has 8 items');
54
54
 
55
55
  collection.insert(1, {country: 'Croatia', firstname: 'Grgur', githubId: 'grgur', lastname: 'Grisogono'});
56
56
 
57
- t.isStrict(collection.getCount(), 9, 'Collection has 9 items');
57
+ t.isStrict(collection.count, 9, 'Collection has 9 items');
58
58
  t.isStrict(collection.map.size, 9, 'map has 9 items');
59
59
 
60
60
  t.isDeeplyStrict(collection.getRange(1, 4), [
@@ -227,7 +227,7 @@ StartTest(t => {
227
227
  collection3.remove('mrsunshine');
228
228
  collection3.remove('elmasse');
229
229
 
230
- t.isStrict(collection3.getCount(), 7, 'collection3 count is 7');
230
+ t.isStrict(collection3.count, 7, 'collection3 count is 7');
231
231
 
232
232
  t.diag("filter by firstname, like, 'a'");
233
233
 
@@ -238,8 +238,8 @@ StartTest(t => {
238
238
  value : 'a'
239
239
  }];
240
240
 
241
- t.isStrict(collection3.getCount(), 4, 'collection3 count is 4');
242
- t.isStrict(collection3.allItems.getCount(), 7, 'collection3 allItems count is 7');
241
+ t.isStrict(collection3.count, 4, 'collection3 count is 4');
242
+ t.isStrict(collection3.allItems.count, 7, 'collection3 allItems count is 7');
243
243
 
244
244
  t.diag("Add Max & Nils back");
245
245
 
@@ -248,8 +248,8 @@ StartTest(t => {
248
248
  {country: 'Germany', firstname: 'Nils', githubId: 'mrsunshine', lastname: 'Dehl'}
249
249
  ]);
250
250
 
251
- t.isStrict(collection3.getCount(), 5, 'collection3 count is 5');
252
- t.isStrict(collection3.allItems.getCount(), 9, 'collection3 allItems count is 9');
251
+ t.isStrict(collection3.count, 5, 'collection3 count is 5');
252
+ t.isStrict(collection3.allItems.count, 9, 'collection3 allItems count is 9');
253
253
  });
254
254
 
255
255
  t.it('Add & remove at same time', t => {