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,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);
|
package/src/vdom/Helper.mjs
CHANGED
@@ -529,16 +529,15 @@ class Helper extends Base {
|
|
529
529
|
* @protected
|
530
530
|
*/
|
531
531
|
insertNode({deltas, index, oldVnodeMap, vnode, vnodeMap}) {
|
532
|
-
let details
|
533
|
-
{parentNode}
|
534
|
-
parentId
|
535
|
-
me
|
536
|
-
movedNodes
|
537
|
-
delta
|
538
|
-
|
539
|
-
|
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
|
package/src/vdom/VNode.mjs
CHANGED
@@ -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.
|
package/src/worker/App.mjs
CHANGED
@@ -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.
|
package/test/siesta/siesta.js
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
242
|
-
t.isStrict(collection3.allItems.
|
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.
|
252
|
-
t.isStrict(collection3.allItems.
|
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 => {
|