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.
Files changed (52) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
  2. package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
  3. package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
  4. package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
  5. package/ServiceWorker.mjs +2 -2
  6. package/apps/portal/index.html +1 -1
  7. package/apps/portal/resources/data/blog.json +24 -0
  8. package/apps/portal/view/ViewportController.mjs +6 -4
  9. package/apps/portal/view/examples/List.mjs +28 -19
  10. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  11. package/examples/functional/button/base/MainContainer.mjs +207 -0
  12. package/examples/functional/button/base/app.mjs +6 -0
  13. package/examples/functional/button/base/index.html +11 -0
  14. package/examples/functional/button/base/neo-config.json +6 -0
  15. package/learn/blog/v10-deep-dive-functional-components.md +293 -0
  16. package/learn/blog/v10-deep-dive-reactivity.md +522 -0
  17. package/learn/blog/v10-deep-dive-state-provider.md +432 -0
  18. package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
  19. package/learn/blog/v10-post1-love-story.md +383 -0
  20. package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
  21. package/package.json +3 -3
  22. package/src/DefaultConfig.mjs +2 -2
  23. package/src/Neo.mjs +47 -45
  24. package/src/component/Abstract.mjs +412 -0
  25. package/src/component/Base.mjs +18 -380
  26. package/src/core/Base.mjs +34 -33
  27. package/src/core/Effect.mjs +30 -34
  28. package/src/core/EffectManager.mjs +101 -14
  29. package/src/core/Observable.mjs +69 -65
  30. package/src/form/field/Text.mjs +11 -5
  31. package/src/functional/button/Base.mjs +384 -0
  32. package/src/functional/component/Base.mjs +51 -145
  33. package/src/layout/Cube.mjs +8 -4
  34. package/src/manager/VDomUpdate.mjs +179 -94
  35. package/src/mixin/VdomLifecycle.mjs +4 -1
  36. package/src/state/Provider.mjs +41 -27
  37. package/src/util/VDom.mjs +11 -4
  38. package/src/util/vdom/TreeBuilder.mjs +38 -62
  39. package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
  40. package/test/siesta/siesta.js +15 -3
  41. package/test/siesta/tests/VdomCalendar.mjs +7 -7
  42. package/test/siesta/tests/VdomHelper.mjs +7 -7
  43. package/test/siesta/tests/classic/Button.mjs +113 -0
  44. package/test/siesta/tests/core/EffectBatching.mjs +46 -41
  45. package/test/siesta/tests/functional/Button.mjs +113 -0
  46. package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
  47. package/test/siesta/tests/vdom/Advanced.mjs +14 -8
  48. package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
  49. package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
  50. package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
  51. package/test/siesta/tests/vdom/table/Container.mjs +9 -5
  52. package/src/core/EffectBatchManager.mjs +0 -67
@@ -0,0 +1,384 @@
1
+ import FunctionalBase from '../../functional/component/Base.mjs';
2
+ import NeoArray from '../../util/Array.mjs';
3
+
4
+ /**
5
+ * @class Neo.functional.button.Base
6
+ * @extends Neo.functional.component.Base
7
+ */
8
+ class Button extends FunctionalBase {
9
+ /**
10
+ * Valid values for badgePosition
11
+ * @member {String[]} badgePositions=['bottom-left','bottom-right','top-left','top-right']
12
+ * @protected
13
+ * @static
14
+ */
15
+ static badgePositions = ['bottom-left', 'bottom-right', 'top-left', 'top-right']
16
+ /**
17
+ * Valid values for iconPosition
18
+ * @member {String[]} iconPositions=['top','right','bottom','left']
19
+ * @protected
20
+ * @static
21
+ */
22
+ static iconPositions = ['top', 'right', 'bottom', 'left']
23
+
24
+ static config = {
25
+ /**
26
+ * @member {String} className='Neo.functional.button.Base'
27
+ * @protected
28
+ */
29
+ className: 'Neo.functional.button.Base',
30
+ /**
31
+ * @member {String} ntype='functional-button'
32
+ * @protected
33
+ */
34
+ ntype: 'functional-button',
35
+ /**
36
+ * @member {String} badgePosition_='top-right'
37
+ * @reactive
38
+ */
39
+ badgePosition_: 'top-right',
40
+ /**
41
+ * @member {String|null} badgeText_=null
42
+ * @reactive
43
+ */
44
+ badgeText_: null,
45
+ /**
46
+ * @member {String[]} baseCls=['neo-button']
47
+ */
48
+ baseCls: ['neo-button'],
49
+ /**
50
+ * false calls Neo.Main.setRoute()
51
+ * @member {Boolean} editRoute=true
52
+ */
53
+ editRoute: true,
54
+ /**
55
+ * @member {Function|String|null} handler_=null
56
+ * @reactive
57
+ */
58
+ handler_: null,
59
+ /**
60
+ * @member {Object|String|null} handlerScope=null
61
+ */
62
+ handlerScope: null,
63
+ /**
64
+ * @member {String[]|null} [iconCls_=null]
65
+ * @reactive
66
+ */
67
+ iconCls_: null,
68
+ /**
69
+ * @member {String|null} iconColor_=null
70
+ * @reactive
71
+ */
72
+ iconColor_: null,
73
+ /**
74
+ * @member {String} iconPosition_='left'
75
+ * @reactive
76
+ */
77
+ iconPosition_: 'left',
78
+ /**
79
+ * @member {Object|Object[]|null} menu_=null
80
+ * @reactive
81
+ */
82
+ menu_: null,
83
+ /**
84
+ * @member {Boolean} pressed_=false
85
+ * @reactive
86
+ */
87
+ pressed_: false,
88
+ /**
89
+ * Internal state for managing the ripple effect animations.
90
+ * Each object in the array requires a unique `id`.
91
+ * @member {Array} ripples_=[]
92
+ * @protected
93
+ * @reactive
94
+ */
95
+ ripples_: [],
96
+ /**
97
+ * @member {String|null} route_=null
98
+ * @reactive
99
+ */
100
+ route_: null,
101
+ /**
102
+ * @member {String} tag='button'
103
+ * @reactive
104
+ */
105
+ tag: 'button',
106
+ /**
107
+ * @member {String|null} text_=null
108
+ * @reactive
109
+ */
110
+ text_: null,
111
+ /**
112
+ * @member {String|null} url_=null
113
+ * @reactive
114
+ */
115
+ url_: null,
116
+ /**
117
+ * @member {String} urlTarget_='_blank'
118
+ * @reactive
119
+ */
120
+ urlTarget_: '_blank',
121
+ /**
122
+ * @member {Boolean} useRippleEffect_=true
123
+ * @reactive
124
+ */
125
+ useRippleEffect_: true
126
+ }
127
+
128
+ /**
129
+ * @member {Number} rippleEffectDuration=400
130
+ */
131
+ rippleEffectDuration = 400
132
+ /**
133
+ * @member {Number|null} rippleCleanupTimeout=null
134
+ * @protected
135
+ */
136
+ rippleCleanupTimeout = null
137
+
138
+ /**
139
+ * @param {Object} config
140
+ */
141
+ construct(config) {
142
+ super.construct(config);
143
+
144
+ let me = this;
145
+
146
+ me.addDomListeners({
147
+ click: me.onClick,
148
+ scope: me
149
+ })
150
+ }
151
+
152
+ /**
153
+ * The single source of truth for the button's VDOM.
154
+ * This method is automatically re-run when any of its dependent configs change.
155
+ * @param {Object} config The component's config object (this instance).
156
+ * @param {Object} data The hierarchical data from state.Provider.
157
+ * @returns {Object}
158
+ */
159
+ createVdom(config, data) {
160
+ const me = this,
161
+ {
162
+ badgePosition, badgeText, cls, editRoute, iconCls, iconColor, iconPosition,
163
+ pressed, ripples, route, style, tag, text, url, urlTarget, useRippleEffect
164
+ } = config;
165
+
166
+ const vdomCls = [...me.baseCls, ...cls || []];
167
+ NeoArray.toggle(vdomCls, 'no-text', !text);
168
+ NeoArray.toggle(vdomCls, 'pressed', pressed);
169
+ vdomCls.push('icon-' + iconPosition);
170
+
171
+ const link = !editRoute && route || url;
172
+ const finalTag = link ? 'a' : tag;
173
+
174
+ return {
175
+ tag : finalTag,
176
+ cls : vdomCls,
177
+ style,
178
+ href : link ? (link.startsWith('#') ? link : '#' + link) : null,
179
+ target: url ? urlTarget : null,
180
+ type : finalTag === 'button' ? 'button' : null,
181
+ cn : [
182
+ // iconNode
183
+ {
184
+ tag : 'span',
185
+ cls : ['neo-button-glyph', ...iconCls || []],
186
+ removeDom: !iconCls,
187
+ style : {color: iconColor || null}
188
+ },
189
+ // textNode
190
+ {
191
+ tag : 'span',
192
+ cls : ['neo-button-text'],
193
+ removeDom: !text,
194
+ text
195
+ },
196
+ // badgeNode
197
+ {
198
+ tag : 'span',
199
+ cls : ['neo-button-badge', 'neo-' + badgePosition],
200
+ removeDom: !badgeText,
201
+ text : badgeText
202
+ },
203
+ // rippleWrapper
204
+ {
205
+ cls : ['neo-button-ripple-wrapper'],
206
+ removeDom: !(useRippleEffect && ripples.length > 0),
207
+ cn : ripples.map(ripple => ({
208
+ cls : ['neo-button-ripple'],
209
+ id : ripple.id,
210
+ style: {
211
+ animation: `ripple ${me.rippleEffectDuration}ms linear`,
212
+ height : `${ripple.diameter}px`,
213
+ left : `${ripple.left}px`,
214
+ top : `${ripple.top}px`,
215
+ width : `${ripple.diameter}px`
216
+ }
217
+ }))
218
+ }
219
+ ]
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @param {Object} data
225
+ */
226
+ onClick(data) {
227
+ let me = this;
228
+
229
+ me.bindCallback(me.handler, 'handler', me.handlerScope || me);
230
+ me.handler?.(data);
231
+
232
+ me.menu && me.toggleMenu();
233
+ me.route && me.changeRoute();
234
+
235
+ if (me.useRippleEffect) {
236
+ const buttonRect = data.path[0].rect;
237
+ const diameter = Math.max(buttonRect.height, buttonRect.width);
238
+ const radius = diameter / 2;
239
+ const rippleId = Neo.getId('ripple');
240
+
241
+ // Add a new ripple to the state
242
+ me.ripples = [...me.ripples, {
243
+ id : rippleId,
244
+ diameter,
245
+ left: data.clientX - buttonRect.left - radius,
246
+ top : data.clientY - buttonRect.top - radius
247
+ }];
248
+
249
+ // Clear any previously scheduled cleanup to ensure only the last
250
+ // click's timer will execute the cleanup.
251
+ clearTimeout(me.rippleCleanupTimeout);
252
+
253
+ // Schedule the cleanup for the entire ripples array.
254
+ me.rippleCleanupTimeout = me.timeout(me.rippleEffectDuration).then(() => {
255
+ me.ripples = [];
256
+ });
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Triggered after the menu config got changed
262
+ * @param {Object|Object[]|null} value
263
+ * @param {Object|Object[]|null} oldValue
264
+ * @protected
265
+ */
266
+ afterSetMenu(value, oldValue) {
267
+ const me = this;
268
+
269
+ if (value) {
270
+ // Ensure menuList is destroyed before creating a new one
271
+ me.menuList?.destroy(true, false);
272
+ me.menuList = null;
273
+
274
+ import('../../menu/List.mjs').then(module => {
275
+ const isArray = Array.isArray(value),
276
+ items = isArray ? value : value.items,
277
+ menuConfig = isArray ? {} : value,
278
+ stateProvider = me.getStateProvider(),
279
+ {appName, theme, windowId} = me,
280
+
281
+ config = Neo.merge({
282
+ module : module.default,
283
+ align : {edgeAlign: 't0-b0', target: me.id},
284
+ appName,
285
+ displayField : 'text',
286
+ floating : true,
287
+ hidden : true,
288
+ parentComponent: me,
289
+ theme,
290
+ windowId
291
+ }, menuConfig);
292
+
293
+ if (items) {
294
+ config.items = items
295
+ }
296
+
297
+ if (stateProvider) {
298
+ config.stateProvider = {parent: stateProvider}
299
+ }
300
+
301
+ me.menuList = Neo.create(config)
302
+ })
303
+ } else if (me.menuList) {
304
+ me.menuList.destroy(true, false);
305
+ me.menuList = null;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Triggered before the badgePosition config gets changed
311
+ * @param {String} value
312
+ * @param {String} oldValue
313
+ * @returns {String}
314
+ * @protected
315
+ */
316
+ beforeSetBadgePosition(value, oldValue) {
317
+ // Using `this.constructor.badgePositions` is superior to `Button.badgePositions` for future class extensions
318
+ return this.beforeSetEnumValue(value, oldValue, 'badgePosition', this.constructor.badgePositions)
319
+ }
320
+
321
+ /**
322
+ * Triggered before the iconCls config gets changed. Converts the string into an array if needed.
323
+ * @param {Array|String|null} value
324
+ * @param {Array|String|null} oldValue
325
+ * @returns {Array|null}
326
+ * @protected
327
+ */
328
+ beforeSetIconCls(value, oldValue) {
329
+ if (value && !Array.isArray(value)) {
330
+ return value.split(' ').filter(Boolean)
331
+ }
332
+
333
+ return value || null
334
+ }
335
+
336
+ /**
337
+ * Triggered before the iconPosition config gets changed
338
+ * @param {String} value
339
+ * @param {String} oldValue
340
+ * @protected
341
+ */
342
+ beforeSetIconPosition(value, oldValue) {
343
+ // Using `this.constructor.iconPositions` is superior to `Button.iconPositions` for future class extensions
344
+ return this.beforeSetEnumValue(value, oldValue, 'iconPosition', this.constructor.iconPositions)
345
+ }
346
+
347
+ /**
348
+ * @protected
349
+ */
350
+ changeRoute() {
351
+ const me = this;
352
+ me.editRoute && Neo.Main.editRoute(me.route)
353
+ }
354
+
355
+ /**
356
+ * @param args
357
+ */
358
+ destroy(...args) {
359
+ const me = this;
360
+
361
+ clearTimeout(me.rippleCleanupTimeout);
362
+ me.menuList?.destroy(true, false);
363
+ super.destroy(...args)
364
+ }
365
+
366
+ /**
367
+ *
368
+ */
369
+ async toggleMenu() {
370
+ const me = this;
371
+ let {menuList} = me,
372
+ hidden = !menuList?.hidden;
373
+
374
+ if (menuList) {
375
+ menuList.hidden = hidden;
376
+
377
+ if (!hidden) {
378
+ await me.timeout(50)
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ export default Neo.setupClass(Button);
@@ -1,10 +1,6 @@
1
- import Base from '../../core/Base.mjs';
2
- import ComponentManager from '../../manager/Component.mjs';
3
- import DomEvents from '../../mixin/DomEvents.mjs';
4
- import Effect from '../../core/Effect.mjs';
5
- import NeoArray from '../../util/Array.mjs';
6
- import Observable from '../../core/Observable.mjs';
7
- import VdomLifecycle from '../../mixin/VdomLifecycle.mjs';
1
+ import Abstract from '../../component/Abstract.mjs';
2
+ import Effect from '../../core/Effect.mjs';
3
+ import NeoArray from '../../util/Array.mjs';
8
4
 
9
5
  const
10
6
  activeDomListenersSymbol = Symbol.for('activeDomListeners'),
@@ -15,12 +11,9 @@ const
15
11
 
16
12
  /**
17
13
  * @class Neo.functional.component.Base
18
- * @extends Neo.core.Base
19
- * @mixes Neo.component.mixin.DomEvents
20
- * @mixes Neo.core.Observable
21
- * @mixes Neo.component.mixin.VdomLifecycle
14
+ * @extends Neo.component.Abstract
22
15
  */
23
- class FunctionalBase extends Base {
16
+ class FunctionalBase extends Abstract {
24
17
  static config = {
25
18
  /**
26
19
  * @member {String} className='Neo.functional.component.Base'
@@ -32,40 +25,11 @@ class FunctionalBase extends Base {
32
25
  * @protected
33
26
  */
34
27
  ntype: 'functional-component',
35
- /**
36
- * Custom CSS selectors to apply to the root level node of this component
37
- * @member {String[]} cls=null
38
- * @reactive
39
- */
40
- cls: null,
41
- /**
42
- * @member {Neo.core.Base[]} mixins=[DomEvents, Observable, VdomLifecycle]
43
- */
44
- mixins: [DomEvents, Observable, VdomLifecycle],
45
- /**
46
- * True after the component render() method was called. Also fires the rendered event.
47
- * @member {Boolean} mounted_=false
48
- * @protected
49
- * @reactive
50
- */
51
- mounted_: false,
52
- /**
53
- * @member {String|null} parentId_=null
54
- * @protected
55
- * @reactive
56
- */
57
- parentId_: null,
58
28
  /**
59
29
  * The vdom markup for this component.
60
30
  * @member {Object} vdom={}
61
31
  */
62
- vdom: {},
63
- /**
64
- * The custom windowIs (timestamp) this component belongs to
65
- * @member {Number|null} windowId_=null
66
- * @reactive
67
- */
68
- windowId_: null
32
+ vdom: {}
69
33
  }
70
34
 
71
35
  /**
@@ -73,12 +37,6 @@ class FunctionalBase extends Base {
73
37
  * @member {Map|null} childComponents=null
74
38
  */
75
39
  childComponents = null
76
- /**
77
- * Internal flag which will get set to true while a component is waiting for its mountedPromise
78
- * @member {Boolean} isAwaitingMount=false
79
- * @protected
80
- */
81
- isAwaitingMount = false
82
40
  /**
83
41
  * Internal Map to store the next set of components after the createVdom() Effect has run.
84
42
  * @member {Map|null} nextChildComponents=null
@@ -86,41 +44,6 @@ class FunctionalBase extends Base {
86
44
  */
87
45
  #nextChildComponents = null
88
46
 
89
- /**
90
- * A Promise that resolves when the component is mounted to the DOM.
91
- * This provides a convenient way to wait for the component to be fully
92
- * available and interactive before executing subsequent logic.
93
- *
94
- * It also handles unmounting by resetting the promise, so it can be safely
95
- * awaited again if the component is remounted.
96
- * @returns {Promise<Neo.component.Base>}
97
- */
98
- get mountedPromise() {
99
- let me = this;
100
-
101
- if (!me._mountedPromise) {
102
- me._mountedPromise = new Promise(resolve => {
103
- if (me.mounted) {
104
- resolve(me);
105
- } else {
106
- me.mountedPromiseResolve = resolve
107
- }
108
- })
109
- }
110
-
111
- return me._mountedPromise
112
- }
113
-
114
- /**
115
- * Convenience method to access the parent component
116
- * @returns {Neo.component.Base|null}
117
- */
118
- get parent() {
119
- let me = this;
120
-
121
- return me.parentComponent || (me.parentId === 'document.body' ? null : Neo.getComponent(me.parentId))
122
- }
123
-
124
47
  /**
125
48
  * @param {Object} config
126
49
  */
@@ -143,7 +66,7 @@ class FunctionalBase extends Base {
143
66
  fn: () => {
144
67
  me[hookIndexSymbol] = 0;
145
68
  me[pendingDomEventsSymbol] = []; // Clear pending events for new render
146
- me[vdomToApplySymbol] = me.createVdom(me, me.data)
69
+ me[vdomToApplySymbol] = me.createVdom(me)
147
70
  },
148
71
  componentId: me.id,
149
72
  subscriber : {
@@ -154,19 +77,6 @@ class FunctionalBase extends Base {
154
77
  })
155
78
  }
156
79
 
157
- /**
158
- * Triggered after the id config got changed
159
- * @param {String|null} value
160
- * @param {String|null} oldValue
161
- * @protected
162
- */
163
- afterSetId(value, oldValue) {
164
- super.afterSetId(value, oldValue);
165
-
166
- oldValue && ComponentManager.unregister(oldValue);
167
- value && ComponentManager.register(this)
168
- }
169
-
170
80
  /**
171
81
  * Triggered after the mounted config got changed
172
82
  * @param {Boolean} value
@@ -174,19 +84,14 @@ class FunctionalBase extends Base {
174
84
  * @protected
175
85
  */
176
86
  afterSetMounted(value, oldValue) {
87
+ super.afterSetMounted(value, oldValue);
88
+
177
89
  if (oldValue !== undefined) {
178
90
  const me = this;
179
91
 
180
92
  if (value) { // mount
181
- me.initDomEvents();
182
-
183
93
  // Initial registration of DOM event listeners when component mounts
184
94
  me.applyPendingDomListeners();
185
-
186
- me.mountedPromiseResolve?.(this);
187
- delete me.mountedPromiseResolve
188
- } else { // unmount
189
- delete me._mountedPromise
190
95
  }
191
96
  }
192
97
  }
@@ -198,21 +103,11 @@ class FunctionalBase extends Base {
198
103
  * @protected
199
104
  */
200
105
  afterSetWindowId(value, oldValue) {
201
- const me = this;
202
-
203
- if (value) {
204
- Neo.currentWorker.insertThemeFiles(value, me.__proto__)
205
- }
106
+ super.afterSetWindowId(value, oldValue);
206
107
 
207
- me.childComponents?.forEach(childData => {
108
+ this.childComponents?.forEach(childData => {
208
109
  childData.instance.windowId = value
209
110
  })
210
-
211
- // If a component gets moved into a different window, an update cycle might still be running.
212
- // Since the update might no longer get mapped, we want to re-enable this instance for future updates.
213
- if (oldValue) {
214
- me.isVdomUpdating = false
215
- }
216
111
  }
217
112
 
218
113
  /**
@@ -242,14 +137,44 @@ class FunctionalBase extends Base {
242
137
  }
243
138
  }
244
139
 
140
+ /**
141
+ * A lifecycle hook that runs after a state change has been detected but before the
142
+ * VDOM update is dispatched. It provides a dedicated place for logic that needs to
143
+ * execute before rendering, such as calculating derived data or caching values.
144
+ *
145
+ * You can prevent the VDOM update by returning `false` from this method. This is
146
+ * useful for advanced cases where you might want to manually trigger a different
147
+ * update after modifying other component configs.
148
+ *
149
+ * **IMPORTANT**: Do not change the value of any config that is used as a dependency
150
+ * within the `createVdom` method from inside this hook, as it will cause an
151
+ * infinite update loop. This hook is for one-way data flow, not for triggering
152
+ * cascading reactive changes.
153
+ *
154
+ * @returns {Boolean|undefined} Return `false` to cancel the upcoming VDOM update.
155
+ * @example
156
+ * beforeUpdate() {
157
+ * // Perform an expensive calculation and cache the result on the instance
158
+ * this.processedData = this.processRawData(this.rawData);
159
+ *
160
+ * // Example of conditionally cancelling an update
161
+ * if (this.processedData.length === 0 && this.vdom.cn?.length === 0) {
162
+ * return false; // Don't re-render if there's nothing to show
163
+ * }
164
+ * }
165
+ */
166
+ beforeUpdate() {
167
+ // This method can be overridden by subclasses
168
+ }
169
+
245
170
  /**
246
171
  * Override this method in your functional component to return its VDOM structure.
247
172
  * This method will be automatically re-executed when any of the component's configs change.
173
+ * To access data from a state provider, use `config.data`.
248
174
  * @param {Neo.functional.component.Base} config - Mental model: while it contains the instance, it makes it clear to access configs
249
- * @param {Object} data - Convenience shortcut for accessing `state.Provider` data
250
175
  * @returns {Object} The VDOM structure for the component.
251
176
  */
252
- createVdom(config, data) {
177
+ createVdom(config) {
253
178
  // This method should be overridden by subclasses
254
179
  return {}
255
180
  }
@@ -268,13 +193,9 @@ class FunctionalBase extends Base {
268
193
  });
269
194
  me.childComponents?.clear();
270
195
 
271
- me.removeDomEvents();
272
-
273
196
  // Remove any pending DOM event listeners that might not have been mounted
274
197
  me[pendingDomEventsSymbol] = null;
275
198
 
276
- ComponentManager.unregister(me);
277
-
278
199
  super.destroy()
279
200
  }
280
201
 
@@ -382,7 +303,14 @@ class FunctionalBase extends Base {
382
303
  root.id = me.id
383
304
  }
384
305
 
385
- me.updateVdom();
306
+ // Re-hydrate the new vdom with stable IDs from the previous vnode tree.
307
+ // This is crucial for functional components where the vdom is recreated on every render,
308
+ // ensuring the diffing algorithm can track nodes correctly.
309
+ me.syncVdomIds();
310
+
311
+ if (me.beforeUpdate() !== false) {
312
+ me.updateVdom()
313
+ }
386
314
 
387
315
  // Update DOM event listeners based on the new render
388
316
  if (me.mounted) {
@@ -472,28 +400,6 @@ class FunctionalBase extends Base {
472
400
 
473
401
  return vdomTree
474
402
  }
475
-
476
- /**
477
- * Change multiple configs at once, ensuring that all afterSet methods get all new assigned values
478
- * @param {Object} values={}
479
- * @param {Boolean} silent=false
480
- * @returns {Promise<*>}
481
- */
482
- set(values={}, silent=false) {
483
- let me = this;
484
-
485
- me.silentVdomUpdate = true;
486
-
487
- super.set(values);
488
-
489
- me.silentVdomUpdate = false;
490
-
491
- if (silent || !me.needsVdomUpdate) {
492
- return Promise.resolve()
493
- }
494
-
495
- return me.promiseUpdate()
496
- }
497
403
  }
498
404
 
499
405
  export default Neo.setupClass(FunctionalBase);
@@ -114,11 +114,15 @@ class Cube extends Card {
114
114
 
115
115
  me.nestVdom();
116
116
 
117
- container.mounted && container.update();
118
-
119
- me.timeout(50).then(() => {
120
- container.addCls('neo-animate')
117
+ me.observeConfig(container, 'mounted', value => {
118
+ value && container.addCls('neo-animate')
121
119
  })
120
+
121
+ if (container.mounted) {
122
+ container.promiseUpdate().then(() => {
123
+ container.addCls('neo-animate')
124
+ })
125
+ }
122
126
  }
123
127
 
124
128
  /**