neo.mjs 10.0.0-beta.6 → 10.0.1
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.1.md +20 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
- package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +24 -0
- package/apps/portal/view/ViewportController.mjs +6 -4
- package/apps/portal/view/examples/List.mjs +28 -19
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/functional/button/base/MainContainer.mjs +207 -0
- package/examples/functional/button/base/app.mjs +6 -0
- package/examples/functional/button/base/index.html +11 -0
- package/examples/functional/button/base/neo-config.json +6 -0
- package/learn/blog/v10-deep-dive-functional-components.md +293 -0
- package/learn/blog/v10-deep-dive-reactivity.md +522 -0
- package/learn/blog/v10-deep-dive-state-provider.md +432 -0
- package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
- package/learn/blog/v10-post1-love-story.md +383 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +47 -45
- package/src/component/Abstract.mjs +412 -0
- package/src/component/Base.mjs +18 -380
- package/src/core/Base.mjs +34 -33
- package/src/core/Effect.mjs +30 -34
- package/src/core/EffectManager.mjs +101 -14
- package/src/core/Observable.mjs +69 -65
- package/src/form/field/Text.mjs +11 -5
- package/src/functional/button/Base.mjs +384 -0
- package/src/functional/component/Base.mjs +51 -145
- package/src/layout/Cube.mjs +8 -4
- package/src/manager/VDomUpdate.mjs +179 -94
- package/src/mixin/VdomLifecycle.mjs +4 -1
- package/src/state/Provider.mjs +41 -27
- package/src/util/VDom.mjs +11 -4
- package/src/util/vdom/TreeBuilder.mjs +38 -62
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
- package/test/siesta/siesta.js +15 -3
- package/test/siesta/tests/VdomCalendar.mjs +7 -7
- package/test/siesta/tests/VdomHelper.mjs +7 -7
- package/test/siesta/tests/classic/Button.mjs +113 -0
- package/test/siesta/tests/core/EffectBatching.mjs +46 -41
- package/test/siesta/tests/functional/Button.mjs +113 -0
- package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
- package/test/siesta/tests/vdom/Advanced.mjs +14 -8
- package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
- package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
- package/test/siesta/tests/vdom/table/Container.mjs +9 -5
- package/src/core/EffectBatchManager.mjs +0 -67
@@ -1,6 +1,34 @@
|
|
1
1
|
import Collection from '../collection/Base.mjs';
|
2
2
|
|
3
3
|
/**
|
4
|
+
* The VDomUpdate manager is a singleton responsible for orchestrating and optimizing
|
5
|
+
* component VDOM updates within the Neo.mjs framework. It acts as a central coordinator
|
6
|
+
* to optimize the VDOM update process. Its primary goal is to reduce the amount of
|
7
|
+
* message roundtrips between the application and VDOM workers by aggregating multiple
|
8
|
+
* component updates into a single, optimized VDOM tree.
|
9
|
+
*
|
10
|
+
* Key Responsibilities:
|
11
|
+
* 1. **Update Merging & Aggregation:** Allows a parent component to absorb the update
|
12
|
+
* requests of its children. Instead of each child triggering a separate VDOM update
|
13
|
+
* message to the VDOM worker, the parent sends a single, aggregated VDOM tree. This
|
14
|
+
* significantly reduces the overhead of worker communication and can result in smaller,
|
15
|
+
* more focused data for the VDOM worker to process. While the amount of final DOM
|
16
|
+
* modifications remains the same, this aggregation is key to performance.
|
17
|
+
*
|
18
|
+
* 2. **Asynchronous Flow Control:** Manages the asynchronous nature of VDOM updates, which
|
19
|
+
* are often processed in a worker thread. It ensures that code awaiting an update
|
20
|
+
* (e.g., via a returned Promise) is correctly notified upon completion.
|
21
|
+
*
|
22
|
+
* 3. **Dependency Chaining:** Provides a "post-update" queue, allowing one component's
|
23
|
+
* update to be declaratively chained to another's, ensuring a predictable order of
|
24
|
+
* operations.
|
25
|
+
*
|
26
|
+
* 4. **State Tracking:** Keeps track of updates that are "in-flight" (i.e., currently
|
27
|
+
* being processed), which helps to avoid race conditions and redundant work.
|
28
|
+
*
|
29
|
+
* By centralizing these concerns, VDomUpdate plays a critical role in the framework's
|
30
|
+
* performance and rendering efficiency.
|
31
|
+
*
|
4
32
|
* @class Neo.manager.VDomUpdate
|
5
33
|
* @extends Neo.collection.Base
|
6
34
|
* @singleton
|
@@ -13,34 +41,68 @@ class VDomUpdate extends Collection {
|
|
13
41
|
*/
|
14
42
|
className: 'Neo.manager.VDomUpdate',
|
15
43
|
/**
|
16
|
-
*
|
17
|
-
*
|
18
|
-
|
19
|
-
|
20
|
-
|
44
|
+
* A collection that maps a parent component's ID (`ownerId`) to the set of child
|
45
|
+
* components whose VDOM updates have been merged into that parent's update cycle.
|
46
|
+
*
|
47
|
+
* The structure for each entry is:
|
48
|
+
* `{ ownerId: 'parent-id', children: Map<'child-id', {childUpdateDepth, distance}> }`
|
49
|
+
*
|
50
|
+
* - `ownerId`: The `id` of the parent component taking responsibility for the update.
|
51
|
+
* - `children`: A Map where keys are the `id`s of the merged children and values
|
52
|
+
* are objects containing metadata needed to calculate the total update scope.
|
53
|
+
*
|
21
54
|
* @member {Neo.collection.Base|null} mergedCallbackMap=null
|
22
55
|
* @protected
|
23
56
|
*/
|
24
57
|
mergedCallbackMap: null,
|
25
58
|
/**
|
59
|
+
* A collection that queues components that need to be updated immediately after
|
60
|
+
* another component's update cycle completes. This is used to handle rendering
|
61
|
+
* dependencies.
|
62
|
+
*
|
63
|
+
* The structure for each entry is:
|
64
|
+
* `{ ownerId: 'component-id', children: [{childId, resolve}] }`
|
65
|
+
*
|
66
|
+
* - `ownerId`: The `id` of the component whose update completion will trigger the queued updates.
|
67
|
+
* - `children`: An array of objects, where `childId` is the component to update and
|
68
|
+
* `resolve` is the Promise resolver to call after that subsequent update is done.
|
69
|
+
*
|
26
70
|
* @member {Neo.collection.Base|null} postUpdateQueueMap=null
|
27
71
|
* @protected
|
28
72
|
*/
|
29
73
|
postUpdateQueueMap: null,
|
74
|
+
/**
|
75
|
+
* @member {Boolean} singleton=true
|
76
|
+
* @protected
|
77
|
+
*/
|
78
|
+
singleton: true
|
30
79
|
}
|
31
80
|
|
32
81
|
/**
|
82
|
+
* A Map that tracks VDOM updates that have been dispatched to the VDOM worker but
|
83
|
+
* have not yet completed. This prevents redundant updates for the same component.
|
84
|
+
*
|
85
|
+
* The structure is: `Map<'component-id', updateDepth>`
|
86
|
+
*
|
33
87
|
* @member {Map|null} inFlightUpdateMap=null
|
34
88
|
* @protected
|
35
89
|
*/
|
36
90
|
inFlightUpdateMap = null;
|
37
91
|
/**
|
92
|
+
* A Map that stores Promise `resolve` functions associated with a component's update.
|
93
|
+
* When a component's VDOM update is finalized, the callbacks for its ID are executed,
|
94
|
+
* resolving the Promise returned by the component's `update()` method.
|
95
|
+
*
|
96
|
+
* The structure is: `Map<'component-id', [callback1, callback2, ...]>`
|
97
|
+
*
|
38
98
|
* @member {Map|null} promiseCallbackMap=null
|
39
99
|
* @protected
|
40
100
|
*/
|
41
101
|
promiseCallbackMap = null;
|
42
102
|
|
43
103
|
/**
|
104
|
+
* Initializes the manager's internal collections and maps.
|
105
|
+
* This is called automatically when the singleton instance is created.
|
44
106
|
* @param {Object} config
|
45
107
|
*/
|
46
108
|
construct(config) {
|
@@ -55,8 +117,11 @@ class VDomUpdate extends Collection {
|
|
55
117
|
}
|
56
118
|
|
57
119
|
/**
|
58
|
-
*
|
59
|
-
*
|
120
|
+
* Registers a callback function to be executed when a specific component's
|
121
|
+
* VDOM update completes. This is the mechanism that resolves the Promise
|
122
|
+
* returned by `Component#update()`.
|
123
|
+
* @param {String} ownerId The `id` of the component owning the update.
|
124
|
+
* @param {Function} callback The function to execute upon completion.
|
60
125
|
*/
|
61
126
|
addPromiseCallback(ownerId, callback) {
|
62
127
|
let me = this;
|
@@ -69,72 +134,53 @@ class VDomUpdate extends Collection {
|
|
69
134
|
}
|
70
135
|
|
71
136
|
/**
|
72
|
-
*
|
73
|
-
*
|
74
|
-
*
|
75
|
-
|
76
|
-
|
77
|
-
this.inFlightUpdateMap.set(ownerId, updateDepth);
|
78
|
-
}
|
79
|
-
|
80
|
-
/**
|
81
|
-
* Retrieves the update depth for an in-flight update.
|
82
|
-
* @param {String} ownerId The ID of the component owning the update.
|
83
|
-
* @returns {Number|undefined} The update depth, or undefined if not found.
|
84
|
-
*/
|
85
|
-
getInFlightUpdateDepth(ownerId) {
|
86
|
-
return this.inFlightUpdateMap.get(ownerId);
|
87
|
-
}
|
88
|
-
|
89
|
-
/**
|
90
|
-
* Unregisters an in-flight update once it has completed.
|
91
|
-
* @param {String} ownerId The ID of the component owning the update.
|
92
|
-
*/
|
93
|
-
unregisterInFlightUpdate(ownerId) {
|
94
|
-
this.inFlightUpdateMap.delete(ownerId);
|
95
|
-
}
|
96
|
-
|
97
|
-
/**
|
98
|
-
* @param {String} ownerId
|
99
|
-
* @param {String} childId
|
100
|
-
* @param {Number} childUpdateDepth
|
101
|
-
* @param {Number} distance
|
137
|
+
* Executes all callbacks associated with a completed VDOM update for a given `ownerId`.
|
138
|
+
* This method first processes callbacks for any children that were merged into this
|
139
|
+
* update cycle, then executes the callbacks for the `ownerId` itself.
|
140
|
+
* @param {String} ownerId The `id` of the component whose update has just completed.
|
141
|
+
* @param {Object} [data] Optional data to pass to the callbacks.
|
102
142
|
*/
|
103
|
-
|
104
|
-
let me
|
105
|
-
item
|
143
|
+
executeCallbacks(ownerId, data) {
|
144
|
+
let me = this,
|
145
|
+
item = me.mergedCallbackMap.get(ownerId),
|
146
|
+
callbackData = data ? [data] : [];
|
106
147
|
|
107
|
-
if (
|
108
|
-
item
|
109
|
-
|
148
|
+
if (item) {
|
149
|
+
item.children.forEach((value, key) => {
|
150
|
+
me.executePromiseCallbacks(key, ...callbackData)
|
151
|
+
});
|
152
|
+
me.mergedCallbackMap.remove(item);
|
110
153
|
}
|
111
154
|
|
112
|
-
|
155
|
+
me.executePromiseCallbacks(ownerId, ...callbackData)
|
113
156
|
}
|
114
157
|
|
115
158
|
/**
|
116
|
-
*
|
117
|
-
*
|
118
|
-
* @param {
|
159
|
+
* A helper method that invokes all registered promise callbacks for a given
|
160
|
+
* component ID and then clears them from the queue.
|
161
|
+
* @param {String} ownerId The `id` of the component.
|
162
|
+
* @param {Object} [data] Optional data to pass to the callbacks.
|
119
163
|
*/
|
120
|
-
|
121
|
-
let me
|
122
|
-
|
123
|
-
childCallbacks;
|
124
|
-
|
125
|
-
if (!item) {
|
126
|
-
item = {ownerId, children: []};
|
127
|
-
me.postUpdateQueueMap.add(item);
|
128
|
-
}
|
164
|
+
executePromiseCallbacks(ownerId, data) {
|
165
|
+
let me = this,
|
166
|
+
callbacks = me.promiseCallbackMap.get(ownerId);
|
129
167
|
|
130
|
-
|
168
|
+
callbacks?.forEach(callback => callback(data));
|
169
|
+
me.promiseCallbackMap.delete(ownerId);
|
131
170
|
}
|
132
171
|
|
133
172
|
/**
|
134
|
-
* Calculates the
|
135
|
-
*
|
136
|
-
*
|
137
|
-
*
|
173
|
+
* Calculates the required `updateDepth` for a parent component based on its own
|
174
|
+
* needs and the needs of all child components whose updates have been merged into it.
|
175
|
+
* The final depth is the maximum required depth to ensure all changes are rendered.
|
176
|
+
*
|
177
|
+
* For example, if a parent needs to update its direct content (`updateDepth: 1`) but
|
178
|
+
* a merged child 3 levels down needs a full subtree update (`childUpdateDepth: -1`),
|
179
|
+
* this method will return -1, signaling a full recursive update from the parent.
|
180
|
+
*
|
181
|
+
* This method is called by the parent component right before it dispatches its VDOM update.
|
182
|
+
* @param {String} ownerId The `id` of the parent component.
|
183
|
+
* @returns {Number|null} The adjusted update depth, or `null` if no merged children exist.
|
138
184
|
*/
|
139
185
|
getAdjustedUpdateDepth(ownerId) {
|
140
186
|
let me = this,
|
@@ -146,71 +192,101 @@ class VDomUpdate extends Collection {
|
|
146
192
|
if (item) {
|
147
193
|
item.children.forEach(value => {
|
148
194
|
if (value.childUpdateDepth === -1) {
|
149
|
-
newDepth = -1
|
195
|
+
newDepth = -1
|
150
196
|
} else {
|
151
197
|
// The new depth is the distance to the child plus the child's own required update depth.
|
152
|
-
newDepth = value.distance + value.childUpdateDepth
|
198
|
+
newDepth = value.distance + value.childUpdateDepth
|
153
199
|
}
|
154
200
|
|
155
201
|
if (newDepth === -1) {
|
156
|
-
maxDepth = -1
|
202
|
+
maxDepth = -1
|
157
203
|
} else if (maxDepth !== -1) {
|
158
|
-
maxDepth = Math.max(maxDepth, newDepth)
|
204
|
+
maxDepth = Math.max(maxDepth, newDepth)
|
159
205
|
}
|
160
206
|
});
|
161
207
|
|
162
|
-
return maxDepth
|
208
|
+
return maxDepth
|
163
209
|
}
|
164
210
|
|
165
|
-
return null
|
211
|
+
return null
|
212
|
+
}
|
213
|
+
|
214
|
+
/**
|
215
|
+
* Retrieves the `updateDepth` for a component's update that is currently in-flight.
|
216
|
+
* @param {String} ownerId The `id` of the component owning the update.
|
217
|
+
* @returns {Number|undefined} The update depth, or `undefined` if no update is in-flight.
|
218
|
+
*/
|
219
|
+
getInFlightUpdateDepth(ownerId) {
|
220
|
+
return this.inFlightUpdateMap.get(ownerId)
|
166
221
|
}
|
167
222
|
|
168
223
|
/**
|
169
|
-
* Returns a Set of child IDs that have been merged into a parent's update.
|
170
|
-
*
|
171
|
-
* @
|
224
|
+
* Returns a Set of child component IDs that have been merged into a parent's update cycle.
|
225
|
+
* This is used by the parent to know which children it is responsible for updating.
|
226
|
+
* @param {String} ownerId The `id` of the parent component.
|
227
|
+
* @returns {Set<String>|null} A Set containing the IDs of the merged children, or `null`.
|
172
228
|
*/
|
173
229
|
getMergedChildIds(ownerId) {
|
174
230
|
const item = this.mergedCallbackMap.get(ownerId);
|
175
231
|
if (item) {
|
176
|
-
return new Set(item.children.keys())
|
232
|
+
return new Set(item.children.keys())
|
177
233
|
}
|
178
|
-
return null
|
234
|
+
return null
|
179
235
|
}
|
180
236
|
|
181
237
|
/**
|
182
|
-
*
|
183
|
-
*
|
238
|
+
* Marks a component's VDOM update as "in-flight," meaning it has been sent to the
|
239
|
+
* worker for processing.
|
240
|
+
* @param {String} ownerId The `id` of the component owning the update.
|
241
|
+
* @param {Number} updateDepth The depth of the in-flight update.
|
184
242
|
*/
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
callbackData = data ? [data] : [];
|
243
|
+
registerInFlightUpdate(ownerId, updateDepth) {
|
244
|
+
this.inFlightUpdateMap.set(ownerId, updateDepth)
|
245
|
+
}
|
189
246
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
247
|
+
/**
|
248
|
+
* Registers a child's update request to be merged into its parent's update cycle.
|
249
|
+
* This is called by a child component when it determines it can delegate its update
|
250
|
+
* to an ancestor.
|
251
|
+
* @param {String} ownerId The `id` of the parent component that will own the merged update.
|
252
|
+
* @param {String} childId The `id` of the child component requesting the merge.
|
253
|
+
* @param {Number} childUpdateDepth The update depth required by the child.
|
254
|
+
* @param {Number} distance The component tree distance (number of levels) between the parent and child.
|
255
|
+
*/
|
256
|
+
registerMerged(ownerId, childId, childUpdateDepth, distance) {
|
257
|
+
let me = this,
|
258
|
+
item = me.mergedCallbackMap.get(ownerId);
|
259
|
+
|
260
|
+
if (!item) {
|
261
|
+
item = {ownerId, children: new Map()};
|
262
|
+
me.mergedCallbackMap.add(item)
|
195
263
|
}
|
196
264
|
|
197
|
-
|
265
|
+
item.children.set(childId, {childUpdateDepth, distance})
|
198
266
|
}
|
199
267
|
|
200
268
|
/**
|
201
|
-
*
|
202
|
-
* @param {
|
269
|
+
* Queues a component update to be executed after another component's update is complete.
|
270
|
+
* @param {String} ownerId The `id` of the component to wait for.
|
271
|
+
* @param {String} childId The `id` of the component to update afterward.
|
272
|
+
* @param {Function} [resolve] The Promise resolver to be called when the `childId`'s subsequent update finishes.
|
203
273
|
*/
|
204
|
-
|
205
|
-
let me
|
206
|
-
|
274
|
+
registerPostUpdate(ownerId, childId, resolve) {
|
275
|
+
let me = this,
|
276
|
+
item = me.postUpdateQueueMap.get(ownerId);
|
207
277
|
|
208
|
-
|
209
|
-
|
278
|
+
if (!item) {
|
279
|
+
item = {ownerId, children: []};
|
280
|
+
me.postUpdateQueueMap.add(item)
|
281
|
+
}
|
282
|
+
|
283
|
+
item.children.push({childId, resolve})
|
210
284
|
}
|
211
285
|
|
212
286
|
/**
|
213
|
-
*
|
287
|
+
* Triggers all pending updates that were queued to run after the specified `ownerId`'s
|
288
|
+
* update has completed.
|
289
|
+
* @param {String} ownerId The `id` of the component whose update has just finished.
|
214
290
|
*/
|
215
291
|
triggerPostUpdates(ownerId) {
|
216
292
|
let me = this,
|
@@ -223,13 +299,22 @@ class VDomUpdate extends Collection {
|
|
223
299
|
|
224
300
|
if (component) {
|
225
301
|
entry.resolve && me.addPromiseCallback(component.id, entry.resolve);
|
226
|
-
component.update()
|
302
|
+
component.update()
|
227
303
|
}
|
228
304
|
});
|
229
305
|
|
230
|
-
me.postUpdateQueueMap.remove(item)
|
306
|
+
me.postUpdateQueueMap.remove(item)
|
231
307
|
}
|
232
308
|
}
|
309
|
+
|
310
|
+
/**
|
311
|
+
* Removes a component's update from the "in-flight" registry. This is called after
|
312
|
+
* the VDOM worker confirms the update has been processed.
|
313
|
+
* @param {String} ownerId The `id` of the component owning the update.
|
314
|
+
*/
|
315
|
+
unregisterInFlightUpdate(ownerId) {
|
316
|
+
this.inFlightUpdateMap.delete(ownerId)
|
317
|
+
}
|
233
318
|
}
|
234
319
|
|
235
320
|
export default Neo.setupClass(VDomUpdate);
|
@@ -440,6 +440,7 @@ class VdomLifecycle extends Base {
|
|
440
440
|
* - you pass true for the mount param
|
441
441
|
* - or the autoMount config is set to true
|
442
442
|
* @param {Boolean} [mount] Mount the DOM after the vnode got created
|
443
|
+
* @returns {Promise<any>} If getting there, we return the data from vdom.Helper: create(), containing the vnode.
|
443
444
|
*/
|
444
445
|
async render(mount) {
|
445
446
|
let me = this,
|
@@ -486,7 +487,9 @@ class VdomLifecycle extends Base {
|
|
486
487
|
|
487
488
|
autoMount && !useVdomWorker && me.mount();
|
488
489
|
|
489
|
-
me.resolveVdomUpdate()
|
490
|
+
me.resolveVdomUpdate();
|
491
|
+
|
492
|
+
return data
|
490
493
|
}
|
491
494
|
}
|
492
495
|
|
package/src/state/Provider.mjs
CHANGED
@@ -2,7 +2,7 @@ import Base from '../core/Base.mjs';
|
|
2
2
|
import ClassSystemUtil from '../util/ClassSystem.mjs';
|
3
3
|
import Config from '../core/Config.mjs';
|
4
4
|
import Effect from '../core/Effect.mjs';
|
5
|
-
import
|
5
|
+
import EffectManager from '../core/EffectManager.mjs';
|
6
6
|
import Observable from '../core/Observable.mjs';
|
7
7
|
import {createHierarchicalDataProxy} from './createHierarchicalDataProxy.mjs';
|
8
8
|
import {isDescriptor} from '../core/ConfigSymbols.mjs';
|
@@ -474,17 +474,6 @@ class Provider extends Base {
|
|
474
474
|
internalSetData(key, value, originStateProvider) {
|
475
475
|
const me = this;
|
476
476
|
|
477
|
-
// If the value is a Neo.data.Record, treat it as an atomic value
|
478
|
-
// and set it directly without further recursive processing of its properties.
|
479
|
-
if (Neo.isRecord(value)) {
|
480
|
-
const
|
481
|
-
ownerDetails = me.getOwnerOfDataProperty(key),
|
482
|
-
targetProvider = ownerDetails ? ownerDetails.owner : (originStateProvider || me);
|
483
|
-
|
484
|
-
me.#setConfigValue(targetProvider, key, value, null);
|
485
|
-
return
|
486
|
-
}
|
487
|
-
|
488
477
|
if (Neo.isObject(key)) {
|
489
478
|
Object.entries(key).forEach(([dataKey, dataValue]) => {
|
490
479
|
me.internalSetData(dataKey, dataValue, originStateProvider)
|
@@ -492,13 +481,28 @@ class Provider extends Base {
|
|
492
481
|
return
|
493
482
|
}
|
494
483
|
|
484
|
+
// Now 'key' is a string path.
|
485
|
+
// If 'value' is a plain object, we need to drill down further.
|
486
|
+
// If the value is a Neo.data.Record, treat it as an atomic value => it will not enter this block.
|
487
|
+
if (Neo.typeOf(value) === 'Object') {
|
488
|
+
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
489
|
+
const fullPath = `${key}.${nestedKey}`;
|
490
|
+
me.internalSetData(fullPath, nestedValue, originStateProvider);
|
491
|
+
});
|
492
|
+
return // We've delegated the setting to deeper paths.
|
493
|
+
}
|
494
|
+
|
495
495
|
const
|
496
496
|
ownerDetails = me.getOwnerOfDataProperty(key),
|
497
497
|
targetProvider = ownerDetails ? ownerDetails.owner : (originStateProvider || me);
|
498
498
|
|
499
499
|
me.#setConfigValue(targetProvider, key, value, null);
|
500
500
|
|
501
|
-
//
|
501
|
+
// This is the "reactivity bubbling" logic. When a leaf property like 'user.name' changes,
|
502
|
+
// we must also trigger effects that depend on the parent object 'user'. We do this by
|
503
|
+
// creating a new object reference for each parent in the path. The spread syntax
|
504
|
+
// `{ ...oldParentValue, [leafKey]: latestValue }` is key, as it creates a new
|
505
|
+
// object, which the reactivity system detects as a change.
|
502
506
|
let path = key,
|
503
507
|
latestValue = value;
|
504
508
|
|
@@ -518,7 +522,11 @@ class Provider extends Base {
|
|
518
522
|
break // Stop if parent is not an object
|
519
523
|
}
|
520
524
|
} else {
|
521
|
-
|
525
|
+
// If the parent config doesn't exist, we need to create it to support bubbling.
|
526
|
+
// This is crucial for creating new nested data structures at runtime.
|
527
|
+
const newParentValue = {[leafKey]: latestValue};
|
528
|
+
me.#setConfigValue(targetProvider, path, newParentValue);
|
529
|
+
latestValue = newParentValue
|
522
530
|
}
|
523
531
|
}
|
524
532
|
}
|
@@ -582,8 +590,8 @@ class Provider extends Base {
|
|
582
590
|
|
583
591
|
/**
|
584
592
|
* @param {Neo.component.Base} component
|
585
|
-
* @param {String}
|
586
|
-
* @param {String}
|
593
|
+
* @param {String} configName
|
594
|
+
* @param {String} storeName
|
587
595
|
*/
|
588
596
|
resolveStore(component, configName, storeName) {
|
589
597
|
let store = this.getStore(storeName);
|
@@ -598,9 +606,9 @@ class Provider extends Base {
|
|
598
606
|
* This method creates a new Config instance if one doesn't exist for the given path,
|
599
607
|
* or updates an existing one. It also triggers binding effects and calls onDataPropertyChange.
|
600
608
|
* @param {Neo.state.Provider} provider The StateProvider instance owning the config.
|
601
|
-
* @param {String}
|
602
|
-
* @param {*}
|
603
|
-
* @param {*}
|
609
|
+
* @param {String} path The full path of the data property (e.g., 'user.firstname').
|
610
|
+
* @param {*} newValue The new value to set.
|
611
|
+
* @param {*} [oldVal] The old value (optional, used for initial setup).
|
604
612
|
* @private
|
605
613
|
*/
|
606
614
|
#setConfigValue(provider, path, newValue, oldVal) {
|
@@ -629,12 +637,15 @@ class Provider extends Base {
|
|
629
637
|
* are run only once.
|
630
638
|
*
|
631
639
|
* @param {Object|String} key
|
632
|
-
* @param {*}
|
640
|
+
* @param {*} value
|
633
641
|
*/
|
634
642
|
setData(key, value) {
|
635
|
-
|
636
|
-
|
637
|
-
|
643
|
+
EffectManager.pause();
|
644
|
+
try {
|
645
|
+
this.internalSetData(key, value, this)
|
646
|
+
} finally {
|
647
|
+
EffectManager.resume()
|
648
|
+
}
|
638
649
|
}
|
639
650
|
|
640
651
|
/**
|
@@ -645,12 +656,15 @@ class Provider extends Base {
|
|
645
656
|
* are run only once.
|
646
657
|
*
|
647
658
|
* @param {Object|String} key
|
648
|
-
* @param {*}
|
659
|
+
* @param {*} value
|
649
660
|
*/
|
650
661
|
setDataAtSameLevel(key, value) {
|
651
|
-
|
652
|
-
|
653
|
-
|
662
|
+
EffectManager.pause();
|
663
|
+
try {
|
664
|
+
this.internalSetData(key, value)
|
665
|
+
} finally {
|
666
|
+
EffectManager.resume()
|
667
|
+
}
|
654
668
|
}
|
655
669
|
}
|
656
670
|
|
package/src/util/VDom.mjs
CHANGED
@@ -391,16 +391,23 @@ class VDom extends Base {
|
|
391
391
|
vdom.id = vnode.id
|
392
392
|
}
|
393
393
|
} else {
|
394
|
-
//
|
395
|
-
//
|
396
|
-
|
394
|
+
// We only want to add an ID if the vdom node does not already have one.
|
395
|
+
// This preserves developer-provided IDs while allowing the framework
|
396
|
+
// to assign IDs to nodes that need them for reconciliation.
|
397
|
+
// Also think of adding and removing nodes in parallel.
|
398
|
+
if (vnode.id && (!vdom.id || vdom.id.startsWith('neo-vnode-'))) {
|
397
399
|
vdom.id = vnode.id
|
398
400
|
}
|
399
401
|
}
|
400
402
|
|
401
403
|
if (childNodes) {
|
402
404
|
cn = childNodes.map(item => VDom.getVdom(item));
|
403
|
-
|
405
|
+
// The vnode.childNodes array is already filtered by the worker.
|
406
|
+
// We must filter the component's vdom.cn array identically to ensure
|
407
|
+
// both arrays are structurally aligned for the sync loop.
|
408
|
+
// The boolean check `item &&` is critical to remove falsy values
|
409
|
+
// from conditional rendering and prevent runtime errors.
|
410
|
+
cn = cn.filter(item => item && item.removeDom !== true);
|
404
411
|
i = 0;
|
405
412
|
len = cn?.length || 0;
|
406
413
|
|