neo.mjs 10.0.0-alpha.3 → 10.0.0-alpha.4

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 (83) hide show
  1. package/.github/CODING_GUIDELINES.md +1 -1
  2. package/README.md +52 -11
  3. package/ServiceWorker.mjs +2 -2
  4. package/apps/portal/index.html +1 -1
  5. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  6. package/apps/portal/view/learn/ContentComponent.mjs +2 -1
  7. package/apps/portal/view/learn/MainContainerStateProvider.mjs +3 -6
  8. package/apps/realworld/view/HomeComponent.mjs +1 -1
  9. package/apps/realworld/view/user/ProfileComponent.mjs +1 -1
  10. package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
  11. package/apps/shareddialog/view/MainContainerController.mjs +2 -2
  12. package/learn/README.md +83 -0
  13. package/learn/guides/ApplicationBootstrap.md +354 -0
  14. package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +500 -0
  15. package/learn/guides/WorkingWithVDom.md +748 -0
  16. package/learn/tree.json +53 -0
  17. package/package.json +2 -2
  18. package/src/DefaultConfig.mjs +27 -14
  19. package/src/Main.mjs +1 -1
  20. package/src/Neo.mjs +16 -0
  21. package/src/button/Base.mjs +2 -2
  22. package/src/calendar/view/MainContainerStateProvider.mjs +1 -1
  23. package/src/grid/header/Button.mjs +1 -1
  24. package/src/layout/Cube.mjs +2 -2
  25. package/src/main/DeltaUpdates.mjs +11 -10
  26. package/src/main/addon/Navigator.mjs +1 -1
  27. package/src/main/addon/WindowPosition.mjs +1 -1
  28. package/src/main/render/StringBasedRenderer.mjs +1 -1
  29. package/src/tab/header/Toolbar.mjs +1 -1
  30. package/src/table/header/Button.mjs +1 -1
  31. package/src/toolbar/Base.mjs +1 -1
  32. package/src/util/VDom.mjs +1 -1
  33. package/src/util/VNode.mjs +1 -1
  34. package/src/vdom/Helper.mjs +96 -49
  35. package/src/vdom/VNode.mjs +38 -2
  36. package/src/worker/App.mjs +2 -1
  37. package/src/worker/Base.mjs +87 -5
  38. package/src/worker/Manager.mjs +86 -28
  39. package/resources/data/deck/learnneo/tree.json +0 -50
  40. package/resources/data/deck/whyneo.md +0 -80
  41. /package/{resources/data/deck/learnneo/pages → learn}/Glossary.md +0 -0
  42. /package/{resources/data/deck/learnneo/pages → learn}/UsingTheseTopics.md +0 -0
  43. /package/{resources/data/deck/learnneo/pages → learn}/benefits/ConfigSystem.md +0 -0
  44. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Effort.md +0 -0
  45. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Features.md +0 -0
  46. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FormsEngine.md +0 -0
  47. /package/{resources/data/deck/learnneo/pages → learn}/benefits/FourEnvironments.md +0 -0
  48. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Introduction.md +0 -0
  49. /package/{resources/data/deck/learnneo/pages → learn}/benefits/MultiWindow.md +0 -0
  50. /package/{resources/data/deck/learnneo/pages → learn}/benefits/OffTheMainThread.md +0 -0
  51. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Quick.md +0 -0
  52. /package/{resources/data/deck/learnneo/pages → learn}/benefits/Speed.md +0 -0
  53. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/ComponentModels.md +0 -0
  54. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Config.md +0 -0
  55. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/DescribingTheUI.md +0 -0
  56. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Events.md +0 -0
  57. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Extending.md +0 -0
  58. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/References.md +0 -0
  59. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Setup.md +0 -0
  60. /package/{resources/data/deck/learnneo/pages → learn}/gettingstarted/Workspaces.md +0 -0
  61. /package/{resources/data/deck/learnneo/pages → learn}/guides/ComponentsAndContainers.md +0 -0
  62. /package/{resources/data/deck/learnneo/pages → learn}/guides/CustomComponents.md +0 -0
  63. /package/{resources/data/deck/learnneo/pages → learn}/guides/Forms.md +0 -0
  64. /package/{resources/data/deck/learnneo/pages → learn}/guides/InstanceLifecycle.md +0 -0
  65. /package/{resources/data/deck/learnneo/pages → learn}/guides/Layouts.md +0 -0
  66. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonExample.md +0 -0
  67. /package/{resources/data/deck/learnneo/pages → learn}/guides/MainThreadAddonIntro.md +0 -0
  68. /package/{resources/data/deck/learnneo/pages → learn}/guides/Mixins.md +0 -0
  69. /package/{resources/data/deck/learnneo/pages → learn}/guides/MultiWindow.md +0 -0
  70. /package/{resources/data/deck/learnneo/pages → learn}/guides/PortalApp.md +0 -0
  71. /package/{resources/data/deck/learnneo/pages → learn}/guides/StateProviders.md +0 -0
  72. /package/{resources/data/deck/learnneo/pages → learn}/guides/Tables.md +0 -0
  73. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/CustomEvents.md +0 -0
  74. /package/{resources/data/deck/learnneo/pages → learn}/guides/events/DomEvents.md +0 -0
  75. /package/{resources/data/deck/learnneo/pages → learn}/javascript/ClassFeatures.md +0 -0
  76. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Classes.md +0 -0
  77. /package/{resources/data/deck/learnneo/pages → learn}/javascript/NewNode.md +0 -0
  78. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Overrides.md +0 -0
  79. /package/{resources/data/deck/learnneo/pages → learn}/javascript/Super.md +0 -0
  80. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/Earthquakes.md +0 -0
  81. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/RSP.md +0 -0
  82. /package/{resources/data/deck/learnneo/pages → learn}/tutorials/TodoList.md +0 -0
  83. /package/resources/data/{deck/learnneo/data/theBeatles.json → theBeatles.json} +0 -0
@@ -0,0 +1,53 @@
1
+ {"data": [
2
+ {"name": "Using These Topics", "parentId": null, "id": "UsingTheseTopics" },
3
+ {"name": "Benefits", "parentId": null, "isLeaf": false, "id": "Benefits"},
4
+ {"name": "Introduction ", "parentId": "Benefits", "id": "benefits.Introduction"},
5
+ {"name": "Off the Main Thread", "parentId": "Benefits", "id": "benefits.OffTheMainThread"},
6
+ {"name": "4 Environments", "parentId": "Benefits", "id": "benefits.FourEnvironments"},
7
+ {"name": "Unified Config System", "parentId": "Benefits", "id": "benefits.ConfigSystem"},
8
+ {"name": "Extreme Speed", "parentId": "Benefits", "id": "benefits.Speed"},
9
+ {"name": "Multi-Window Applications", "parentId": "Benefits", "id": "benefits.MultiWindow"},
10
+ {"name": "Quick Application Development", "parentId": "Benefits", "id": "benefits.Quick"},
11
+ {"name": "Complexity and Effort", "parentId": "Benefits", "id": "benefits.Effort"},
12
+ {"name": "Forms Engine", "parentId": "Benefits", "id": "benefits.FormsEngine"},
13
+ {"name": "Features and Benefits Summary", "parentId": "Benefits", "id": "benefits.Features"},
14
+ {"name": "Getting Started", "parentId": null, "isLeaf": false, "id": "GettingStarted", "collapsed": true},
15
+ {"name": "Setup", "parentId": "GettingStarted", "id": "gettingstarted.Setup"},
16
+ {"name": "Workspaces and Applications", "parentId": "GettingStarted", "id": "gettingstarted.Workspaces"},
17
+ {"name": "Describing a View", "parentId": "GettingStarted", "id": "gettingstarted.DescribingTheUI"},
18
+ {"name": "Events", "parentId": "GettingStarted", "id": "gettingstarted.Events"},
19
+ {"name": "Component References", "parentId": "GettingStarted", "id": "gettingstarted.References"},
20
+ {"name": "Extending Classes", "parentId": "GettingStarted", "id": "gettingstarted.Extending"},
21
+ {"name": "Config", "parentId": "GettingStarted", "id": "gettingstarted.Config"},
22
+ {"name": "Shared Bindable Data", "parentId": "GettingStarted", "id": "gettingstarted.ComponentModels"},
23
+ {"name": "Guides", "parentId": null, "isLeaf": false, "id": "InDepth", "collapsed": true},
24
+ {"name": "Application Bootstrap", "parentId": "InDepth", "id": "guides.ApplicationBootstrap"},
25
+ {"name": "Declarative Component Trees VS Imperative Vdom", "parentId": "InDepth", "id": "guides.DeclarativeComponentTreesVsImperativeVdom"},
26
+ {"name": "Working with VDom", "parentId": "InDepth", "id": "guides.WorkingWithVDom"},
27
+ {"name": "Instance Lifecycle", "parentId": "InDepth", "id": "guides.InstanceLifecycle", "hidden": true},
28
+ {"name": "User Input (Forms)", "parentId": "InDepth", "id": "guides.Forms", "hidden": true},
29
+ {"name": "Component and Container Basics", "parentId": "InDepth", "id": "guides.ComponentsAndContainers"},
30
+ {"name": "Layouts", "parentId": "InDepth", "isLeaf": false, "id": "guides.Layouts", "hidden": true},
31
+ {"name": "Shared Bindable Data (State Providers)", "parentId": "InDepth", "id": "guides.StateProviders"},
32
+ {"name": "Custom Components", "parentId": "InDepth", "id": "guides.CustomComponents", "hidden": true},
33
+ {"name": "Events", "parentId": "InDepth", "isLeaf": false, "id": "GuideEvents"},
34
+ {"name": "Custom Events", "parentId": "GuideEvents", "id": "guides.events.CustomEvents"},
35
+ {"name": "DOM Events", "parentId": "GuideEvents", "id": "guides.events.DomEvents"},
36
+ {"name": "Portal App", "parentId": "InDepth", "id": "guides.PortalApp"},
37
+ {"name": "Tables (Stores)", "parentId": "InDepth", "id": "guides.Tables", "hidden": true},
38
+ {"name": "Multi-Window Applications", "parentId": "InDepth", "id": "guides.MultiWindow", "hidden": true},
39
+ {"name": "Main Thread Addons", "parentId": "InDepth", "isLeaf": false, "id": "MainThreadAddons", "hidden": true},
40
+ {"name": "Introduction", "parentId": "MainThreadAddons", "id": "guides.MainThreadAddonIntro"},
41
+ {"name": "Example", "parentId": "MainThreadAddons", "id": "guides.MainThreadAddonExample"},
42
+ {"name": "Mixins", "parentId": "InDepth", "id": "guides.Mixins", "hidden": true},
43
+ {"name": "Tutorials", "parentId": null, "isLeaf": false, "id": "Tutorials", "collapsed": true},
44
+ {"name": "Rock Scissors Paper", "parentId": "Tutorials", "id": "tutorials.RSP", "hidden": true},
45
+ {"name": "Earthquakes", "parentId": "Tutorials", "id": "tutorials.Earthquakes"},
46
+ {"name": "Todo List", "parentId": "Tutorials", "id": "tutorials.TodoList"},
47
+ {"name": "JavaScript Classes", "parentId": null, "isLeaf": false, "id": "JavaScript", "hidden": true},
48
+ {"name": "Classes, Properties, and Methods", "parentId": "JavaScript", "id": "javascript.Classes"},
49
+ {"name": "Overriding Methods", "parentId": "JavaScript", "id": "javascript.Overrides"},
50
+ {"name": "Other JavaScript Class Features", "parentId": "JavaScript", "id": "javascript.ClassFeatures"},
51
+ {"name": "Super", "parentId": "JavaScript", "id": "javascript.Super"},
52
+ {"name": "New Node", "parentId": "JavaScript", "id": "javascript.NewNode"}
53
+ ]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name" : "neo.mjs",
3
- "version" : "10.0.0-alpha.3",
3
+ "version" : "10.0.0-alpha.4",
4
4
  "description" : "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type" : "module",
6
6
  "repository" : {
@@ -94,7 +94,7 @@
94
94
  "postcss" : "^8.5.6",
95
95
  "sass" : "^1.89.2",
96
96
  "siesta-lite" : "5.5.2",
97
- "terser" : "^5.43.0",
97
+ "terser" : "^5.43.1",
98
98
  "url" : "^0.11.4",
99
99
  "webpack" : "^5.99.9",
100
100
  "webpack-cli" : "^6.0.1",
@@ -200,6 +200,31 @@ const DefaultConfig = {
200
200
  * @type Boolean
201
201
  */
202
202
  useCanvasWorker: false,
203
+ /**
204
+ * `true` will enable the advanced, secure, and performant direct DOM API rendering strategy (recommended).
205
+ * In this mode, `Neo.vdom.Helper` will create and send structured VNode object graphs to the Main Thread.
206
+ * `Neo.main.DeltaUpdates` will then use `Neo.main.render.DomApiRenderer` to directly manipulate the DOM.
207
+ * Crucially, `Neo.main.render.DomApiRenderer` builds new **DOM subtrees** (from the received VNode object graphs)
208
+ * as detached DocumentFragments or elements, entirely outside the live DOM tree.
209
+ * These fully constructed fragments are then inserted into the live document in a **single, atomic operation**.
210
+ * This approach inherently minimizes costly browser reflows/repaints, drastically reduces Cross-Site Scripting (XSS) risks,
211
+ * and optimizes for surgical, atomic DOM updates for unparalleled performance.
212
+ *
213
+ * `false` will enable the legacy string-based rendering strategy.
214
+ * In this mode, `Neo.vdom.Helper` will generate complete HTML strings (`outerHTML`) for VNode subtrees.
215
+ * `Neo.main.DeltaUpdates` will then use `Neo.main.render.StringBasedRenderer` to insert these
216
+ * strings into the DOM using methods like `parentNode.insertAdjacentHTML()`.
217
+ * While performant for large insertions, this mode is generally less secure due to potential XSS vectors
218
+ * and relies on browser HTML parsing, which can be less efficient for granular updates.
219
+ *
220
+ * This configuration affects both the initial painting of your applications and the creation
221
+ * of new component trees at runtime.
222
+ * @default true
223
+ * @memberOf! module:Neo
224
+ * @name config.useDomApiRenderer
225
+ * @type Boolean
226
+ */
227
+ useDomApiRenderer: true,
203
228
  /**
204
229
  * Flag if vdom ids should get mapped into DOM element ids.
205
230
  * false will convert them into a "data-neo-id" attribute.
@@ -246,18 +271,6 @@ const DefaultConfig = {
246
271
  * @type Boolean
247
272
  */
248
273
  useSharedWorkers: false,
249
- /**
250
- * `true` will let the `vdom.Helper` create a String-based representation of the vnode tree.
251
- * Main will then use e.g.`parentNode.insertAdjacentHTML('beforeend', delta.outerHTML);`
252
- * This affects the initial painting of your apps, but also the creation of new component trees at run-time.
253
- * `false` will skip the creation of the String, and instead use DOM APIs to generate a fragment inside Main,
254
- * into which the vnode tree will get applied.
255
- * @default false
256
- * @memberOf! module:Neo
257
- * @name config.useStringBasedMounting
258
- * @type Boolean
259
- */
260
- useStringBasedMounting: false,
261
274
  /**
262
275
  * True will generate a new task worker, which can get filled with own expensive remote methods
263
276
  * @default false
@@ -276,12 +289,12 @@ const DefaultConfig = {
276
289
  useVdomWorker: true,
277
290
  /**
278
291
  * buildScripts/injectPackageVersion.mjs will update this value
279
- * @default '10.0.0-alpha.3'
292
+ * @default '10.0.0-alpha.4'
280
293
  * @memberOf! module:Neo
281
294
  * @name config.version
282
295
  * @type String
283
296
  */
284
- version: '10.0.0-alpha.3'
297
+ version: '10.0.0-alpha.4'
285
298
  };
286
299
 
287
300
  Object.assign(DefaultConfig, {
package/src/Main.mjs CHANGED
@@ -42,7 +42,7 @@ class Main extends core.Base {
42
42
  readQueue: [],
43
43
  /**
44
44
  * Remote method access for other workers
45
- * @member {Object} remote={app: [//...]}
45
+ * @member {Object} remote
46
46
  * @protected
47
47
  */
48
48
  remote: {
package/src/Neo.mjs CHANGED
@@ -436,6 +436,22 @@ Neo = globalThis.Neo = Object.assign({
436
436
  return Neo.create(className, config)
437
437
  },
438
438
 
439
+ /**
440
+ * Updates the global Neo.config object across all active workers and connected browser windows.
441
+ *
442
+ * This is the unified entry point for changing global framework configurations.
443
+ * The framework automatically handles the complex multi-threaded and multi-window
444
+ * synchronization (via App Workers and Shared Workers, if active), ensuring
445
+ * consistency across the entire application without boilerplate.
446
+ *
447
+ * You can pass a partial config object to update specific keys.
448
+ * For nested objects, Neo.mjs performs a deep merge to preserve existing properties.
449
+ *
450
+ * @memberOf module:Neo
451
+ * @function setGlobalConfig
452
+ * @param {Object} config The partial or full Neo.config object with changes to apply.
453
+ */
454
+
439
455
  /**
440
456
  * Internally used at the end of each class / module definition
441
457
  * @memberOf module:Neo
@@ -225,8 +225,8 @@ class Button extends Component {
225
225
  afterSetBadgeText(value, oldValue) {
226
226
  let {badgeNode} = this;
227
227
 
228
- badgeNode.html = value;
229
228
  badgeNode.removeDom = !Boolean(value);
229
+ badgeNode.text = value;
230
230
 
231
231
  this.update()
232
232
  }
@@ -380,7 +380,7 @@ class Button extends Component {
380
380
  textNode.removeDom = isEmpty;
381
381
 
382
382
  if (!isEmpty) {
383
- textNode.html = value
383
+ textNode.text = value
384
384
  }
385
385
 
386
386
  me.update()
@@ -154,7 +154,7 @@ class MainContainerStateProvider extends StateProvider {
154
154
 
155
155
  let {data} = this;
156
156
 
157
- switch(key) {
157
+ switch (key) {
158
158
  case 'locale': {
159
159
  data.intlFormat_time = new Intl.DateTimeFormat(value, data.timeFormat);
160
160
  break
@@ -96,7 +96,7 @@ class Button extends BaseButton {
96
96
  {cls} = me,
97
97
  container = me.up('grid-container');
98
98
 
99
- switch(value) {
99
+ switch (value) {
100
100
  case null:
101
101
  NeoArray.add(cls, 'neo-sort-hidden');
102
102
  break
@@ -258,7 +258,7 @@ class Cube extends Card {
258
258
  if (index < 6) {
259
259
  wrapperCls = NeoArray.union(wrapperCls, 'neo-face', Object.keys(Cube.faces)[index]);
260
260
 
261
- switch(index) {
261
+ switch (index) {
262
262
  case 0:
263
263
  case 1:
264
264
  wrapperCls = NeoArray.union(wrapperCls, 'neo-face-z');
@@ -341,7 +341,7 @@ class Cube extends Card {
341
341
  if (index < 6) {
342
342
  NeoArray.remove(wrapperCls, ['neo-face', Object.keys(Cube.faces)[index]]);
343
343
 
344
- switch(index) {
344
+ switch (index) {
345
345
  case 0:
346
346
  case 1:
347
347
  NeoArray.remove(wrapperCls, 'neo-face-z');
@@ -54,7 +54,6 @@ class DeltaUpdates extends Base {
54
54
  * @private
55
55
  */
56
56
  #renderer = null
57
-
58
57
  /**
59
58
  * Private property to signal that the renderer module has been loaded.
60
59
  * This will be a Promise that resolves when the module is ready.
@@ -87,10 +86,10 @@ class DeltaUpdates extends Base {
87
86
  try {
88
87
  let module;
89
88
 
90
- if (NeoConfig.useStringBasedMounting) {
91
- module = await import('./render/StringBasedRenderer.mjs')
92
- } else {
89
+ if (NeoConfig.useDomApiRenderer) {
93
90
  module = await import('./render/DomApiRenderer.mjs')
91
+ } else {
92
+ module = await import('./render/StringBasedRenderer.mjs')
94
93
  }
95
94
 
96
95
  me.#renderer = module.default
@@ -175,7 +174,7 @@ class DeltaUpdates extends Base {
175
174
  * - `insertAdjacentHTML()` is generally faster than creating a node via template,
176
175
  * but it's only available for manipulating children (elements), not `childNodes` (all nodes).
177
176
  * - For performance, in cases where there are no comment nodes (i.e., no wrapped text nodes),
178
- * the method prioritizes `insertAdjacentHTML()` when `useStringBasedMounting` is true.
177
+ * the method prioritizes `insertAdjacentHTML()` when `useDomApiRenderer` is false.
179
178
  *
180
179
  * @param {Object} delta
181
180
  * @param {Boolean} delta.hasLeadingTextChildren Flag to honor leading comments, which require special treatment.
@@ -185,8 +184,10 @@ class DeltaUpdates extends Base {
185
184
  * @param {Neo.vdom.VNode} [delta.vnode] The VNode representation of the new node (for direct DOM API mounting).
186
185
  */
187
186
  insertNode({hasLeadingTextChildren, index, outerHTML, parentId, vnode}) {
187
+ let me = this;
188
+
188
189
  // This method is synchronous and *expects* the renderer to be loaded
189
- if (!this.#renderer) {
190
+ if (!me.#renderer) {
190
191
  console.error('DeltaUpdates renderer not ready during insertNode!');
191
192
  return
192
193
  }
@@ -194,10 +195,10 @@ class DeltaUpdates extends Base {
194
195
  const parentNode = DomAccess.getElementOrBody(parentId);
195
196
 
196
197
  if (parentNode) {
197
- if (!NeoConfig.useStringBasedMounting) {
198
- this.#renderer.createDomTree({index, isRoot: true, parentNode, vnode})
198
+ if (NeoConfig.useDomApiRenderer) {
199
+ me.#renderer.createDomTree({index, isRoot: true, parentNode, vnode})
199
200
  } else {
200
- this.#renderer.insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode})
201
+ me.#renderer.insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode})
201
202
  }
202
203
  }
203
204
  }
@@ -331,7 +332,7 @@ class DeltaUpdates extends Base {
331
332
 
332
333
  if (node) {
333
334
  Object.entries(delta).forEach(([prop, value]) => {
334
- switch(prop) {
335
+ switch (prop) {
335
336
  case 'attributes':
336
337
  Object.entries(value).forEach(([key, val]) => {
337
338
  if (voidAttributes.has(key)) {
@@ -212,7 +212,7 @@ class Navigator extends Base {
212
212
  let {key, target} = keyEvent,
213
213
  newActiveElement;
214
214
 
215
- switch(key) {
215
+ switch (key) {
216
216
  // Move to the previous navigable item
217
217
  case data.previousKey:
218
218
  newActiveElement = me.navigateGetAdjacent(-1, data);
@@ -156,7 +156,7 @@ class WindowPosition extends Base {
156
156
  {screenLeft, screenTop} = win,
157
157
  left, top;
158
158
 
159
- switch(data.dock) {
159
+ switch (data.dock) {
160
160
  case 'bottom':
161
161
  left = screenLeft;
162
162
  top = win.outerHeight + screenTop - 62;
@@ -11,7 +11,7 @@ const StringBasedRenderer = {
11
11
 
12
12
  /**
13
13
  * Handles string-based insertion of a new node into the DOM.
14
- * This method is called by `insertNode()` when `NeoConfig.useStringBasedMounting` is true.
14
+ * This method is called by `insertNode()` when `NeoConfig.useDomApiRenderer` is false.
15
15
  *
16
16
  * @param {Object} data
17
17
  * @param {Boolean} data.hasLeadingTextChildren Flag to honor leading comments.
@@ -92,7 +92,7 @@ class Toolbar extends BaseToolbar {
92
92
  getLayoutConfig() {
93
93
  let layoutConfig;
94
94
 
95
- switch(this.dock) {
95
+ switch (this.dock) {
96
96
  case 'bottom':
97
97
  case 'top':
98
98
  layoutConfig = {
@@ -121,7 +121,7 @@ class Button extends BaseButton {
121
121
  {cls} = me,
122
122
  container = me.up('table-container');
123
123
 
124
- switch(value) {
124
+ switch (value) {
125
125
  case null:
126
126
  NeoArray.add(cls, 'neo-sort-hidden');
127
127
  break
@@ -177,7 +177,7 @@ class Toolbar extends Container {
177
177
  layoutConfig;
178
178
 
179
179
  if (me.dock) {
180
- switch(me.dock) {
180
+ switch (me.dock) {
181
181
  case 'bottom':
182
182
  case 'top':
183
183
  layoutConfig = {
package/src/util/VDom.mjs CHANGED
@@ -66,7 +66,7 @@ class VDom extends Base {
66
66
 
67
67
  optsArray.forEach(([key, value]) => {
68
68
  if (vdom.hasOwnProperty(key)) {
69
- switch(key) {
69
+ switch (key) {
70
70
  case 'cls':
71
71
  if (typeof value === 'string' && Neo.isArray(vdom[key])) {
72
72
  if (vdom[key].includes(value)) {
@@ -43,7 +43,7 @@ class VNode extends Base {
43
43
 
44
44
  optsArray.forEach(([key, value]) => {
45
45
  if (vnode.hasOwnProperty(key)) {
46
- switch(key) {
46
+ switch (key) {
47
47
  case 'attributes':
48
48
  if (Neo.isObject(value) && Neo.isObject(vnode[key])) {
49
49
  Object.entries(value).forEach(([attrKey, attrValue]) => {
@@ -37,6 +37,26 @@ class Helper extends Base {
37
37
  singleton: true
38
38
  }
39
39
 
40
+ /**
41
+ * @param {Object} config
42
+ */
43
+ construct(config) {
44
+ super.construct(config);
45
+
46
+ let me = this;
47
+
48
+ // Ensure Neo.currentWorker is defined before attaching listeners
49
+ Promise.resolve().then(async () => {
50
+ // Subscribe to global Neo.config changes for dynamic renderer switching.
51
+ Neo.currentWorker.on({
52
+ neoConfigChange: me.onNeoConfigChange,
53
+ scope : me
54
+ });
55
+
56
+ await me.importUtil()
57
+ })
58
+ }
59
+
40
60
  /**
41
61
  * @param {Object} config
42
62
  * @param {Object} config.deltas
@@ -65,11 +85,11 @@ class Helper extends Base {
65
85
  keys = Object.keys(vnode);
66
86
 
67
87
  Object.keys(oldVnode).forEach(prop => {
68
- if (!vnode.hasOwnProperty(prop)) {
88
+ if (!Object.hasOwn(vnode, prop)) {
69
89
  keys.push(prop)
70
- } else if (prop === 'attributes') { // find removed attributes
90
+ } else if (prop === 'attributes') { // Find removed attributes
71
91
  Object.keys(oldVnode[prop]).forEach(attr => {
72
- if (!vnode[prop].hasOwnProperty(attr)) {
92
+ if (!Object.hasOwn(vnode[prop], attr)) {
73
93
  vnode[prop][attr] = null
74
94
  }
75
95
  })
@@ -85,13 +105,22 @@ class Helper extends Base {
85
105
  attributes = {};
86
106
 
87
107
  Object.entries(value).forEach(([key, value]) => {
88
- if (!(oldVnode.attributes.hasOwnProperty(key) && oldVnode.attributes[key] === value)) {
89
- if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
90
- // ignore empty arrays & objects
91
- } else {
92
- attributes[key] = value
93
- }
108
+ const
109
+ oldValue = oldVnode.attributes[key],
110
+ hasOldValue = Object.hasOwn(oldVnode.attributes, 'key');
111
+
112
+ // If the attribute has an old value AND the value hasn't changed, skip.
113
+ if (hasOldValue && oldValue === value) {
114
+ return
115
+ }
116
+
117
+ // If the current value is null, or it's a non-string empty value (e.g., [], {}), skip.
118
+ // Note: An empty string ('') is a valid value and should NOT be skipped here.
119
+ if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
120
+ return
94
121
  }
122
+
123
+ attributes[key] = value
95
124
  });
96
125
 
97
126
  if (Object.keys(attributes).length > 0) {
@@ -148,7 +177,7 @@ class Helper extends Base {
148
177
  /**
149
178
  * Creates a Neo.vdom.VNode tree for the given vdom template.
150
179
  * The top level vnode contains the outerHTML as a string,
151
- * in case Neo.config.useStringBasedMounting === true
180
+ * in case Neo.config.useDomApiRenderer === false
152
181
  * @param {Object} opts
153
182
  * @param {String} opts.appName
154
183
  * @param {Boolean} [opts.autoMount]
@@ -156,22 +185,24 @@ class Helper extends Base {
156
185
  * @param {Number} opts.parentIndex
157
186
  * @param {Object} opts.vdom
158
187
  * @param {Number} opts.windowId
159
- * @returns {Promise<Object>}
188
+ * @returns {Object}
160
189
  */
161
- async create(opts) {
162
- let me = this,
190
+ create(opts) {
191
+ let me = this,
192
+ {util} = Neo.vdom,
163
193
  returnValue, vnode;
164
194
 
165
- await me.importDomApiVnodeCreator();
166
- await me.importStringFromVnode();
167
-
168
195
  vnode = me.createVnode(opts.vdom);
169
196
  returnValue = {...opts, vnode};
170
197
 
171
198
  delete returnValue.vdom;
172
199
 
173
- if (NeoConfig.useStringBasedMounting) {
174
- returnValue.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode)
200
+ if (!NeoConfig.useDomApiRenderer) {
201
+ if (!util.StringFromVnode) {
202
+ throw new Error('VDom Helper render utilities are not loaded yet!')
203
+ }
204
+
205
+ returnValue.outerHTML = util.StringFromVnode.create(vnode)
175
206
  }
176
207
 
177
208
  return returnValue
@@ -303,10 +334,13 @@ class Helper extends Base {
303
334
  if (value !== undefined && value !== null && key !== 'flag' && key !== 'removeDom') {
304
335
  let hasUnit, newValue, style;
305
336
 
306
- switch(key) {
337
+ switch (key) {
307
338
  case 'tag':
308
339
  node.nodeName = value;
309
340
  break
341
+ case 'cls':
342
+ node.className = value;
343
+ break
310
344
  case 'html':
311
345
  node.innerHTML = value.toString(); // support for numbers
312
346
  break
@@ -333,13 +367,7 @@ class Helper extends Base {
333
367
 
334
368
  node.childNodes = newValue;
335
369
  break
336
- case 'cls':
337
- if (value && !Array.isArray(value)) {
338
- node.className = [value]
339
- } else if (!(Array.isArray(value) && value.length < 1)) {
340
- node.className = value
341
- }
342
- break
370
+
343
371
  case 'data':
344
372
  if (value && Neo.typeOf(value) === 'Object') {
345
373
  Object.entries(value).forEach(([key, val]) => {
@@ -472,24 +500,23 @@ class Helper extends Base {
472
500
  }
473
501
 
474
502
  /**
475
- * Only import for the DOM API based mount adapter.
503
+ * Imports either (if not already imported):
504
+ * `Neo.vdom.util.DomApiVnodeCreator` if Neo.config.useDomApiRenderer === true
505
+ * `Neo.vdom.util.StringFromVnode` if Neo.config.useDomApiRenderer === false
476
506
  * @returns {Promise<void>}
477
507
  * @protected
478
508
  */
479
- async importDomApiVnodeCreator() {
480
- if (!NeoConfig.useStringBasedMounting && !Neo.vdom.util?.DomApiVnodeCreator) {
481
- await import('./util/DomApiVnodeCreator.mjs')
482
- }
483
- }
509
+ async importUtil() {
510
+ const {util} = Neo.vdom;
484
511
 
485
- /**
486
- * Only import for the string based mount adapter.
487
- * @returns {Promise<void>}
488
- * @protected
489
- */
490
- async importStringFromVnode() {
491
- if (NeoConfig.useStringBasedMounting && !Neo.vdom.util?.StringFromVnode) {
492
- await import('./util/StringFromVnode.mjs')
512
+ if (NeoConfig.useDomApiRenderer) {
513
+ if (!util?.DomApiVnodeCreator) {
514
+ await import('./util/DomApiVnodeCreator.mjs')
515
+ }
516
+ } else {
517
+ if (!util?.StringFromVnode) {
518
+ await import('./util/StringFromVnode.mjs')
519
+ }
493
520
  }
494
521
  }
495
522
 
@@ -514,12 +541,12 @@ class Helper extends Base {
514
541
 
515
542
  Object.assign(delta, {hasLeadingTextChildren, index: physicalIndex});
516
543
 
517
- if (NeoConfig.useStringBasedMounting) {
518
- // For string-based mounting, pass a string excluding moved nodes
519
- delta.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode, movedNodes)
520
- } else {
544
+ if (NeoConfig.useDomApiRenderer) {
521
545
  // For direct DOM API mounting, pass the pruned VNode tree
522
546
  delta.vnode = Neo.vdom.util.DomApiVnodeCreator.create(vnode, movedNodes)
547
+ } else {
548
+ // For string-based mounting, pass a string excluding moved nodes
549
+ delta.outerHTML = Neo.vdom.util.StringFromVnode.create(vnode, movedNodes)
523
550
  }
524
551
 
525
552
  deltas.default.push(delta);
@@ -603,6 +630,18 @@ class Helper extends Base {
603
630
  this.createDeltas({deltas, oldVnode: movedNode.vnode, oldVnodeMap, vnode, vnodeMap})
604
631
  }
605
632
 
633
+ /**
634
+ * Handler for global Neo.config changes.
635
+ * If 'useDomApiRenderer' property changes, this method dynamically loads/clears the renderer utilities.
636
+ * @param {Object} config
637
+ * @return {Promise<void>}
638
+ */
639
+ async onNeoConfigChange(config) {
640
+ if(Object.hasOwn(config, 'useDomApiRenderer')) {
641
+ await this.importUtil()
642
+ }
643
+ }
644
+
606
645
  /**
607
646
  * @param {Object} config
608
647
  * @param {Object} config.deltas
@@ -633,14 +672,22 @@ class Helper extends Base {
633
672
  * @param {Object} opts
634
673
  * @param {Object} opts.vdom
635
674
  * @param {Object} opts.vnode
636
- * @returns {Promise<Object>}
675
+ * @returns {Object}
637
676
  */
638
- async update(opts) {
639
- let me = this,
677
+ update(opts) {
678
+ let me = this,
679
+ {util} = Neo.vdom,
640
680
  deltas, vnode;
641
681
 
642
- await me.importDomApiVnodeCreator();
643
- await me.importStringFromVnode();
682
+ if (NeoConfig.useDomApiRenderer) {
683
+ if (!util.DomApiVnodeCreator) {
684
+ throw new Error('Neo.vdom.Helper: DomApiVnodeCreator is not loaded yet for updates!')
685
+ }
686
+ } else {
687
+ if (!util.StringFromVnode) {
688
+ throw new Error('Neo.vdom.Helper: StringFromVnode is not loaded yet for updates!');
689
+ }
690
+ }
644
691
 
645
692
  vnode = me.createVnode(opts.vdom);
646
693
  deltas = me.createDeltas({oldVnode: opts.vnode, vnode});