neo.mjs 10.2.1 → 10.3.0

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 (79) hide show
  1. package/.github/CONCEPT.md +2 -4
  2. package/.github/GETTING_STARTED.md +72 -51
  3. package/.github/RELEASE_NOTES/v10.3.0.md +54 -0
  4. package/.github/epic-string-based-templates.md +690 -0
  5. package/ServiceWorker.mjs +2 -2
  6. package/apps/covid/view/MainContainer.mjs +1 -1
  7. package/apps/covid/view/country/Table.mjs +1 -1
  8. package/apps/portal/index.html +1 -1
  9. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  10. package/apps/portal/view/learn/ContentComponent.mjs +1 -1
  11. package/apps/realworld/api/Base.mjs +2 -2
  12. package/apps/sharedcovid/view/MainContainer.mjs +1 -1
  13. package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
  14. package/buildScripts/buildESModules.mjs +23 -75
  15. package/buildScripts/bundleParse5.mjs +27 -0
  16. package/buildScripts/util/astTemplateProcessor.mjs +210 -0
  17. package/buildScripts/util/templateBuildProcessor.mjs +331 -0
  18. package/buildScripts/util/vdomToString.mjs +46 -0
  19. package/buildScripts/webpack/development/webpack.config.appworker.mjs +11 -0
  20. package/buildScripts/webpack/loader/template-loader.mjs +21 -0
  21. package/buildScripts/webpack/production/webpack.config.appworker.mjs +11 -0
  22. package/examples/README.md +1 -1
  23. package/examples/component/wrapper/googleMaps/MarkerDialog.mjs +2 -2
  24. package/examples/form/field/email/MainContainer.mjs +0 -1
  25. package/examples/form/field/number/MainContainer.mjs +0 -1
  26. package/examples/form/field/picker/MainContainer.mjs +0 -1
  27. package/examples/form/field/time/MainContainer.mjs +0 -1
  28. package/examples/form/field/trigger/copyToClipboard/MainContainer.mjs +0 -1
  29. package/examples/form/field/url/MainContainer.mjs +0 -1
  30. package/examples/functional/nestedTemplateComponent/Component.mjs +100 -0
  31. package/examples/functional/nestedTemplateComponent/MainContainer.mjs +48 -0
  32. package/examples/functional/nestedTemplateComponent/app.mjs +6 -0
  33. package/examples/functional/nestedTemplateComponent/index.html +11 -0
  34. package/examples/functional/nestedTemplateComponent/neo-config.json +6 -0
  35. package/examples/functional/templateComponent/Component.mjs +61 -0
  36. package/examples/functional/templateComponent/MainContainer.mjs +48 -0
  37. package/examples/functional/templateComponent/app.mjs +6 -0
  38. package/examples/functional/templateComponent/index.html +11 -0
  39. package/examples/functional/templateComponent/neo-config.json +6 -0
  40. package/learn/gettingstarted/Setup.md +29 -12
  41. package/learn/guides/fundamentals/ApplicationBootstrap.md +2 -2
  42. package/learn/guides/fundamentals/InstanceLifecycle.md +5 -5
  43. package/learn/guides/uibuildingblocks/HtmlTemplates.md +191 -0
  44. package/learn/guides/uibuildingblocks/HtmlTemplatesUnderTheHood.md +156 -0
  45. package/learn/guides/uibuildingblocks/WorkingWithVDom.md +1 -1
  46. package/learn/tree.json +2 -0
  47. package/package.json +62 -56
  48. package/src/DefaultConfig.mjs +3 -3
  49. package/src/calendar/view/calendars/List.mjs +1 -1
  50. package/src/calendar/view/month/Component.mjs +1 -1
  51. package/src/calendar/view/week/Component.mjs +1 -1
  52. package/src/component/Abstract.mjs +1 -1
  53. package/src/component/Base.mjs +33 -27
  54. package/src/container/Base.mjs +5 -5
  55. package/src/controller/Application.mjs +5 -5
  56. package/src/dialog/Base.mjs +6 -6
  57. package/src/draggable/DragProxyComponent.mjs +4 -4
  58. package/src/form/field/ComboBox.mjs +1 -1
  59. package/src/functional/_export.mjs +2 -1
  60. package/src/functional/component/Base.mjs +142 -93
  61. package/src/functional/util/HtmlTemplateProcessor.mjs +243 -0
  62. package/src/functional/util/html.mjs +24 -67
  63. package/src/list/Base.mjs +2 -2
  64. package/src/manager/Toast.mjs +1 -1
  65. package/src/menu/List.mjs +1 -1
  66. package/src/mixin/VdomLifecycle.mjs +87 -90
  67. package/src/tab/Container.mjs +2 -2
  68. package/src/tooltip/Base.mjs +1 -1
  69. package/src/tree/Accordion.mjs +2 -2
  70. package/src/worker/App.mjs +7 -7
  71. package/test/components/files/component/Base.mjs +1 -1
  72. package/test/siesta/siesta.js +2 -0
  73. package/test/siesta/tests/classic/Button.mjs +5 -5
  74. package/test/siesta/tests/functional/Button.mjs +6 -6
  75. package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +193 -33
  76. package/test/siesta/tests/functional/Parse5Processor.mjs +82 -0
  77. package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +5 -5
  78. package/.github/epic-functional-components.md +0 -498
  79. package/.github/ticket-asymmetric-vdom-updates.md +0 -122
@@ -1,6 +1,7 @@
1
- import Abstract from '../../component/Abstract.mjs';
2
- import Effect from '../../core/Effect.mjs';
3
- import NeoArray from '../../util/Array.mjs';
1
+ import Abstract from '../../component/Abstract.mjs';
2
+ import Effect from '../../core/Effect.mjs';
3
+ import NeoArray from '../../util/Array.mjs';
4
+ import {isHtmlTemplate} from '../util/html.mjs';
4
5
 
5
6
  const
6
7
  activeDomListenersSymbol = Symbol.for('activeDomListeners'),
@@ -44,6 +45,11 @@ class FunctionalBase extends Abstract {
44
45
  * @member {Map|null} childComponents=null
45
46
  */
46
47
  childComponents = null
48
+ /**
49
+ * @member {Neo.functional.util.HtmlTemplateProcessor|null} htmlTemplateProcessor=null
50
+ * @private
51
+ */
52
+ htmlTemplateProcessor = Neo.ns('Neo.functional.util.HtmlTemplateProcessor')
47
53
  /**
48
54
  * Internal Map to store the next set of components after the createVdom() Effect has run.
49
55
  * @member {Map|null} nextChildComponents=null
@@ -60,6 +66,11 @@ class FunctionalBase extends Abstract {
60
66
  let me = this,
61
67
  opts = {configurable: true, enumerable: false, writable: true};
62
68
 
69
+ // The build process will replace `render()` with `createVdom()`.
70
+ if (Neo.config.environment !== 'development') {
71
+ me.enableHtmlTemplates = false
72
+ }
73
+
63
74
  Object.defineProperties(me, {
64
75
  [activeDomListenersSymbol]: {...opts, value: []},
65
76
  [hookIndexSymbol] : {...opts, value: 0},
@@ -85,35 +96,32 @@ class FunctionalBase extends Abstract {
85
96
  }
86
97
 
87
98
  /**
88
- * Triggered after the mounted config got changed
99
+ * Triggered after the isReady config got changed
89
100
  * @param {Boolean} value
90
101
  * @param {Boolean} oldValue
91
102
  * @protected
92
103
  */
93
- afterSetMounted(value, oldValue) {
94
- super.afterSetMounted(value, oldValue);
95
-
96
- if (oldValue !== undefined) {
97
- const me = this;
104
+ afterSetIsReady(value, oldValue) {
105
+ const me = this;
98
106
 
99
- if (value) { // mount
100
- // Initial registration of DOM event listeners when component mounts
101
- me.applyPendingDomListeners();
102
- }
107
+ if (value && me.missedReadyState) {
108
+ me.vdomEffect.run();
109
+ delete me.missedReadyState
103
110
  }
104
111
  }
105
112
 
106
113
  /**
107
- * Triggered after the enableHtmlTemplates config got changed.
114
+ * Triggered after the mounted config got changed
108
115
  * @param {Boolean} value
109
116
  * @param {Boolean} oldValue
110
117
  * @protected
111
118
  */
112
- afterSetEnableHtmlTemplates_(value, oldValue) {
113
- if (value && !this.htmlParser) {
114
- import('../util/html.mjs').then(module => {
115
- this.htmlParser = module.default;
116
- });
119
+ afterSetMounted(value, oldValue) {
120
+ super.afterSetMounted(value, oldValue);
121
+
122
+ if (value && oldValue !== undefined) {
123
+ // Initial registration of DOM event listeners when component mounts
124
+ this.applyPendingDomListeners()
117
125
  }
118
126
  }
119
127
 
@@ -188,6 +196,89 @@ class FunctionalBase extends Abstract {
188
196
  // This method can be overridden by subclasses
189
197
  }
190
198
 
199
+ /**
200
+ * This method is called by the HtmlTemplateProcessor after the async parsing is complete.
201
+ * It then continues the component update lifecycle.
202
+ * @param {Object} parsedVdom The VDOM object received from the parser addon.
203
+ * @protected
204
+ */
205
+ continueUpdateWithVdom(parsedVdom) {
206
+ const me = this;
207
+
208
+ // Create a new map for components instantiated in this render cycle
209
+ me.#nextChildComponents = new Map();
210
+
211
+ // Process the newVdom to instantiate components
212
+ // The parentId for these components will be the functional component's id
213
+ const processedVdom = me.processVdomForComponents(parsedVdom, me.id);
214
+
215
+ // Destroy components that are no longer present in the new VDOM
216
+ if (me.childComponents?.size > 0) {
217
+ [...me.childComponents].forEach(([key, childData]) => {
218
+ if (!me.#nextChildComponents.has(key)) {
219
+ me.childComponents.delete(key);
220
+ childData.instance.destroy()
221
+ }
222
+ })
223
+ }
224
+
225
+ // If this component created other classic or functional components,
226
+ // include their full vdom into the next update cycle.
227
+ const oldKeys = me.childComponents ? new Set(me.childComponents.keys()) : new Set();
228
+ let hasNewChildren = false;
229
+
230
+ for (const newKey of me.#nextChildComponents.keys()) {
231
+ if (!oldKeys.has(newKey)) {
232
+ hasNewChildren = true;
233
+ break
234
+ }
235
+ }
236
+
237
+ if (hasNewChildren) {
238
+ // When new child components are created, we need to send their full VDOM
239
+ // to the vdom-worker, so they can get rendered.
240
+ // Subsequent updates will be granular via diffAndSet() => set() on the child.
241
+ me.updateDepth = -1;
242
+ }
243
+
244
+ // Update the main map of instantiated components
245
+ me.childComponents = me.#nextChildComponents;
246
+
247
+ // Clear the old vdom properties
248
+ for (const key in me.vdom) {
249
+ delete me.vdom[key]
250
+ }
251
+
252
+ // Assign the new properties
253
+ Object.assign(me.vdom, processedVdom); // Use processedVdom here
254
+
255
+ me[vdomToApplySymbol] = null;
256
+
257
+ const root = me.getVdomRoot();
258
+
259
+ if (me.cls) {
260
+ root.cls = NeoArray.union(me.cls, root.cls)
261
+ }
262
+
263
+ if (me.id) {
264
+ root.id = me.id
265
+ }
266
+
267
+ // Re-hydrate the new vdom with stable IDs from the previous vnode tree.
268
+ // This is crucial for functional components where the vdom is recreated on every render,
269
+ // ensuring the diffing algorithm can track nodes correctly.
270
+ me.syncVdomIds();
271
+
272
+ if (me.beforeUpdate() !== false) {
273
+ me.updateVdom()
274
+ }
275
+
276
+ // Update DOM event listeners based on the new render
277
+ if (me.mounted) {
278
+ me.applyPendingDomListeners()
279
+ }
280
+ }
281
+
191
282
  /**
192
283
  * Override this method in your functional component to return its VDOM structure.
193
284
  * This method will be automatically re-executed when any of the component's configs change.
@@ -198,10 +289,10 @@ class FunctionalBase extends Abstract {
198
289
  createVdom(config) {
199
290
  const me = this;
200
291
 
201
- if (me.enableHtmlTemplates && typeof me.createTemplateVdom === 'function') {
202
- return me.createTemplateVdom(config)
292
+ if (me.enableHtmlTemplates && typeof me.render === 'function') {
293
+ return me.render(config)
203
294
  }
204
- // This method should be overridden by subclasses
295
+
205
296
  return {}
206
297
  }
207
298
 
@@ -257,6 +348,20 @@ class FunctionalBase extends Abstract {
257
348
  }
258
349
  }
259
350
 
351
+ /**
352
+ * @returns {Promise<void>}
353
+ */
354
+ async initAsync() {
355
+ await super.initAsync();
356
+
357
+ if (this.enableHtmlTemplates && Neo.config.environment === 'development') {
358
+ if (!Neo.ns('Neo.functional.util.HtmlTemplateProcessor')) {
359
+ const module = await import('../util/HtmlTemplateProcessor.mjs');
360
+ this.htmlTemplateProcessor = module.default
361
+ }
362
+ }
363
+ }
364
+
260
365
  /**
261
366
  * This handler runs when the effect's `isRunning` state changes.
262
367
  * It runs outside the effect's tracking scope, preventing feedback loops.
@@ -270,78 +375,22 @@ class FunctionalBase extends Abstract {
270
375
  newVdom = me[vdomToApplySymbol];
271
376
 
272
377
  if (newVdom) {
273
- // Create a new map for components instantiated in this render cycle
274
- me.#nextChildComponents = new Map();
275
-
276
- // Process the newVdom to instantiate components
277
- // The parentId for these components will be the functional component's id
278
- const processedVdom = me.processVdomForComponents(newVdom, me.id);
279
-
280
- // Destroy components that are no longer present in the new VDOM
281
- if (me.childComponents?.size > 0) {
282
- [...me.childComponents].forEach(([key, childData]) => {
283
- if (!me.#nextChildComponents.has(key)) {
284
- me.childComponents.delete(key);
285
- childData.instance.destroy()
286
- }
287
- })
288
- }
289
-
290
- // If this component created other classic or functional components,
291
- // include their full vdom into the next update cycle.
292
- const oldKeys = me.childComponents ? new Set(me.childComponents.keys()) : new Set();
293
- let hasNewChildren = false;
294
-
295
- for (const newKey of me.#nextChildComponents.keys()) {
296
- if (!oldKeys.has(newKey)) {
297
- hasNewChildren = true;
298
- break
378
+ // If the result is an HtmlTemplate, hand it off to the processor
379
+ if (newVdom[isHtmlTemplate]) {
380
+ if (me.htmlTemplateProcessor) {
381
+ me.htmlTemplateProcessor.process(newVdom, me)
382
+ } else {
383
+ me.missedReadyState = true;
384
+ // By calling this with an empty object, we ensure that the parent container
385
+ // renders a placeholder DOM node for this component, which we can then
386
+ // populate later once the template processor is ready.
387
+ me.continueUpdateWithVdom({})
299
388
  }
389
+ return // Stop execution, the processor will call back
300
390
  }
301
391
 
302
- if (hasNewChildren) {
303
- // When new child components are created, we need to send their full VDOM
304
- // to the vdom-worker, so they can get rendered.
305
- // Subsequent updates will be granular via diffAndSet() => set() on the child.
306
- me.updateDepth = -1;
307
- }
308
-
309
- // Update the main map of instantiated components
310
- me.childComponents = me.#nextChildComponents;
311
-
312
- // Clear the old vdom properties
313
- for (const key in me.vdom) {
314
- delete me.vdom[key]
315
- }
316
-
317
- // Assign the new properties
318
- Object.assign(me.vdom, processedVdom); // Use processedVdom here
319
-
320
- me[vdomToApplySymbol] = null;
321
-
322
- const root = me.getVdomRoot();
323
-
324
- if (me.cls) {
325
- root.cls = NeoArray.union(me.cls, root.cls)
326
- }
327
-
328
- if (me.id) {
329
- root.id = me.id
330
- }
331
-
332
- // Re-hydrate the new vdom with stable IDs from the previous vnode tree.
333
- // This is crucial for functional components where the vdom is recreated on every render,
334
- // ensuring the diffing algorithm can track nodes correctly.
335
- me.syncVdomIds();
336
-
337
- if (me.beforeUpdate() !== false) {
338
- me.updateVdom()
339
- }
340
-
341
- // Update DOM event listeners based on the new render
342
- if (me.mounted) {
343
- me.applyPendingDomListeners()
344
- }
392
+ // Continue with the standard JSON-based VDOM update
393
+ me.continueUpdateWithVdom(newVdom)
345
394
  }
346
395
  }
347
396
  }
@@ -374,8 +423,8 @@ class FunctionalBase extends Abstract {
374
423
 
375
424
  if (!componentKey) {
376
425
  console.error([
377
- 'Component definition in functional component VDOM is missing an "id". For stable reconciliation, ',
378
- 'especially in dynamic lists, provide a unique "id" property.'
426
+ 'Component definition in functional component VDOM is missing an "id". For stable reconciliation, ',
427
+ 'especially in dynamic lists, provide a unique "id" property.'
379
428
  ].join(''),
380
429
  vdomTree
381
430
  )
@@ -0,0 +1,243 @@
1
+ import Base from '../../../src/core/Base.mjs';
2
+ import {HtmlTemplate} from './html.mjs';
3
+ import * as parse5 from '../../../dist/parse5.mjs';
4
+
5
+ // This file uses several regular expressions to parse and transform the template string.
6
+ // Defining them as constants at the module level is a performance best practice,
7
+ // as it prevents them from being re-created on every function call.
8
+ const
9
+ // Finds an attribute name right before an interpolated value (e.g., `... testText="${...}"`)
10
+ // This is crucial for preserving the original mixed-case spelling of attributes.
11
+ regexAttribute = /\s+([a-zA-Z][^=]*)\s*=\s*"?$/,
12
+ // Finds a placeholder for a dynamic value that is the entire attribute value.
13
+ regexDynamicValue = /^__DYNAMIC_VALUE_(\d+)__$/,
14
+ // Finds all dynamic value placeholders within a string (globally).
15
+ regexDynamicValueG = /__DYNAMIC_VALUE_(\d+)__/g,
16
+ // Finds placeholders for nested templates or dynamic component tags to re-index them.
17
+ regexNested = /(__DYNAMIC_VALUE_|neotag)(\d+)/g,
18
+ // Extracts the original tag name from its source code string to preserve case sensitivity.
19
+ regexOriginalTagName = /<([\w\.]+)/,
20
+ // parse5 cannot handle self-closing tags, this regex will add closing tags for all custom components
21
+ selfClosingComponentRegex = /<((?:[A-Z][\w\.]*)|(?:neotag\d+))([^>]*?)\/>/g;
22
+
23
+ /**
24
+ * A singleton class responsible for processing HtmlTemplate objects.
25
+ * The core challenge is to convert a tagged template literal (which is just strings and values)
26
+ * into a valid Neo.mjs VDOM structure. This requires several steps:
27
+ * 1. Flattening nested templates into a single string with placeholders.
28
+ * 2. Using a robust HTML parser (`parse5`) to create an Abstract Syntax Tree (AST).
29
+ * 3. Traversing the AST and converting it back into a Neo.mjs VDOM object, re-inserting
30
+ * the original dynamic values (like functions, objects, and components) in the correct places.
31
+ * @class Neo.functional.util.HtmlTemplateProcessor
32
+ * @extends Neo.core.Base
33
+ * @singleton
34
+ */
35
+ class HtmlTemplateProcessor extends Base {
36
+ static config = {
37
+ /**
38
+ * @member {String} className='Neo.functional.util.HtmlTemplateProcessor'
39
+ * @protected
40
+ */
41
+ className: 'Neo.functional.util.HtmlTemplateProcessor',
42
+ /**
43
+ * @member {Boolean} singleton=true
44
+ * @protected
45
+ */
46
+ singleton: true
47
+ }
48
+
49
+ /**
50
+ * Recursively converts a single parse5 AST node into a Neo.mjs VDOM node.
51
+ * This is the heart of the transformation process.
52
+ * @param {Object} node The parse5 AST node
53
+ * @param {Array<*>} values The array of interpolated values from the flattened template
54
+ * @param {String} originalString The flattened template string
55
+ * @param {Object} attributeNameMap A map of dynamic value indices to original, case-sensitive attribute names
56
+ * @returns {Object|String|null} A VDOM node, a text string, or null if the node is empty
57
+ */
58
+ convertNodeToVdom(node, values, originalString, attributeNameMap) {
59
+ // 1. Handle text nodes: Convert text content, re-inserting any dynamic values.
60
+ if (node.nodeName === '#text') {
61
+ const text = node.value.trim();
62
+ if (!text) return null;
63
+
64
+ const replacedText = text.replace(regexDynamicValueG, (m, i) => {
65
+ const value = values[parseInt(i, 10)];
66
+ return Neo.isObject(value) ? JSON.stringify(value) : value
67
+ });
68
+
69
+ return {vtype: 'text', text: replacedText}
70
+ }
71
+
72
+ // 2. Handle element nodes: This is where most of the logic resides.
73
+ if (node.nodeName && node.sourceCodeLocation?.startTag) {
74
+ const
75
+ vdom = {},
76
+ tagName = node.tagName;
77
+
78
+ // A `neotag` is a placeholder for a dynamically injected component constructor (e.g., `<${Button}>`).
79
+ if (tagName.startsWith('neotag')) {
80
+ const index = parseInt(tagName.replace('neotag', ''), 10);
81
+ vdom.module = values[index]
82
+ } else {
83
+ // parse5 lowercases all tag names. To support case-sensitive component tags (e.g., `<MyComponent>`),
84
+ // we must retrieve the original tag name from the source string.
85
+ const
86
+ {startTag} = node.sourceCodeLocation,
87
+ startTagStr = originalString.substring(startTag.startOffset, startTag.endOffset),
88
+ originalTagName = startTagStr.match(regexOriginalTagName)[1];
89
+
90
+ // By convention, a tag starting with an uppercase letter is a Neo.mjs component.
91
+ if (originalTagName[0] === originalTagName[0].toUpperCase()) {
92
+ const resolvedModule = Neo.ns(originalTagName, false);
93
+
94
+ if (resolvedModule) {
95
+ vdom.module = resolvedModule
96
+ } else {
97
+ throw new Error(`Could not resolve component tag <${originalTagName}> from global namespace.`)
98
+ }
99
+ } else {
100
+ vdom.tag = originalTagName
101
+ }
102
+ }
103
+
104
+ // Re-construct attributes, re-inserting dynamic values and preserving original case.
105
+ node.attrs?.forEach(attr => {
106
+ const match = attr.value.match(regexDynamicValue);
107
+ // If the entire attribute is a dynamic value, we can directly assign the rich data type (object, array, function).
108
+ if (match) {
109
+ const
110
+ dynamicValueIndex = parseInt(match[1], 10),
111
+ attrName = attributeNameMap[dynamicValueIndex] || attr.name; // Use the map
112
+
113
+ vdom[attrName] = values[dynamicValueIndex]
114
+ } else {
115
+ // If the attribute is a mix of strings and dynamic values, we must coerce everything to a string.
116
+ vdom[attr.name] = attr.value.replace(regexDynamicValueG, (m, i) => values[parseInt(i, 10)])
117
+ }
118
+ });
119
+
120
+ // Recursively process child nodes.
121
+ if (node.childNodes?.length > 0) {
122
+ vdom.cn = node.childNodes.map(child => this.convertNodeToVdom(child, values, originalString, attributeNameMap)).filter(Boolean);
123
+
124
+ // Optimization: If a node has only one child and it's a text node, we can simplify the VDOM
125
+ // by moving the text content directly into the parent's `text` property.
126
+ if (vdom.cn.length === 1 && vdom.cn[0].vtype === 'text') {
127
+ vdom.text = vdom.cn[0].text;
128
+ delete vdom.cn
129
+ }
130
+ }
131
+
132
+ return vdom
133
+ }
134
+
135
+ return null
136
+ }
137
+
138
+ /**
139
+ * Kicks off the AST to VDOM conversion for the entire template.
140
+ * @param {Object} ast The root parse5 AST
141
+ * @param {Array<*>} values Interpolated values
142
+ * @param {String} originalString The flattened template string
143
+ * @param {Object} attributeNameMap The original attribute names with mixed case, mapped by dynamic value index
144
+ * @returns {Object} The final Neo.mjs VDOM
145
+ */
146
+ convertAstToVdom(ast, values, originalString, attributeNameMap) {
147
+ if (!ast.childNodes || ast.childNodes.length < 1) {
148
+ return {}
149
+ }
150
+
151
+ const children = ast.childNodes.map(child => this.convertNodeToVdom(child, values, originalString, attributeNameMap)).filter(Boolean);
152
+
153
+ // If the template has only one root node, we return it directly.
154
+ // Otherwise, we return a fragment-like object with children in a `cn` array.
155
+ if (children.length === 1) {
156
+ return children[0]
157
+ }
158
+
159
+ return {cn: children}
160
+ }
161
+
162
+ /**
163
+ * Flattens a potentially nested HtmlTemplate object into a single string and a corresponding array of values.
164
+ * This is a necessary pre-processing step before parsing with parse5, which only accepts a single string.
165
+ * @param {Neo.functional.util.HtmlTemplate} template The root template object
166
+ * @returns {{flatString: string, flatValues: Array<*>, attributeNameMap: Object}}
167
+ */
168
+ flattenTemplate(template) {
169
+ let flatString = '';
170
+ const
171
+ flatValues = [],
172
+ attributeNameMap = {};
173
+
174
+ for (let i = 0; i < template.strings.length; i++) {
175
+ let str = template.strings[i];
176
+ flatString += str;
177
+
178
+ if (i < template.values.length) {
179
+ const
180
+ value = template.values[i],
181
+ attrMatch = str.match(regexAttribute);
182
+
183
+ // If a value is another HtmlTemplate, we recursively flatten it and merge the results.
184
+ if (value instanceof HtmlTemplate) {
185
+ const nested = this.flattenTemplate(value);
186
+ // The indices of the nested template's values need to be shifted to avoid collisions.
187
+ const nestedString = nested.flatString.replace(regexNested, (match, p1, p2) => {
188
+ const
189
+ oldIndex = parseInt(p2, 10),
190
+ newIndex = oldIndex + flatValues.length;
191
+ // Adjust keys in nested.attributeNameMap before merging
192
+ if (nested.attributeNameMap[oldIndex]) {
193
+ attributeNameMap[newIndex] = nested.attributeNameMap[oldIndex]
194
+ }
195
+ return `${p1}${newIndex}`
196
+ });
197
+
198
+ flatString += nestedString;
199
+ flatValues.push(...nested.flatValues)
200
+ // No need to push nested.attributeNames, as we merged the map above
201
+ }
202
+ // Falsy values like false, null, and undefined should not be rendered.
203
+ // This is crucial for enabling conditional rendering with `&&`.
204
+ else if (value !== false && value != null) {
205
+ // If a dynamic value is a component constructor, we replace it with a special `neotag` placeholder.
206
+ if (template.strings[i].trim().endsWith('<') || template.strings[i].trim().endsWith('</')) {
207
+ flatString += `neotag${flatValues.length}`
208
+ } else {
209
+ flatString += `__DYNAMIC_VALUE_${flatValues.length}__`
210
+ }
211
+
212
+ flatValues.push(value);
213
+ if (attrMatch) {
214
+ // Store attribute name by its dynamic value index
215
+ attributeNameMap[flatValues.length - 1] = attrMatch[1]
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ return {flatString, flatValues, attributeNameMap};
222
+ }
223
+
224
+ /**
225
+ * The main entry point for processing a template.
226
+ * It orchestrates the flattening, parsing, and VDOM conversion, and then passes the result
227
+ * back to the component to continue its update lifecycle.
228
+ * @param {Neo.functional.util.HtmlTemplate} template The root template object
229
+ * @param {Neo.functional.component.Base} component The component instance
230
+ */
231
+ process(template, component) {
232
+ const
233
+ me = this,
234
+ {flatString, flatValues, attributeNameMap} = me.flattenTemplate(template), // Change variable name
235
+ stringWithClosingTags = flatString.replace(selfClosingComponentRegex, '<$1$2></$1>'),
236
+ ast = parse5.parseFragment(stringWithClosingTags, {sourceCodeLocationInfo: true}),
237
+ parsedVdom = me.convertAstToVdom(ast, flatValues, stringWithClosingTags, attributeNameMap);
238
+
239
+ component.continueUpdateWithVdom(parsedVdom)
240
+ }
241
+ }
242
+
243
+ export default Neo.setupClass(HtmlTemplateProcessor);
@@ -1,75 +1,32 @@
1
- import { rawDimensionTags, voidAttributes, voidElements } from '../../vdom/domConstants.mjs';
1
+ const isHtmlTemplate = Symbol.for('neo.isHtmlTemplate');
2
2
 
3
3
  /**
4
+ * A container for the result of an `html` tagged template literal.
5
+ * It holds the static strings and the dynamic values of the template.
6
+ * @class Neo.functional.util.HtmlTemplate
7
+ */
8
+ class HtmlTemplate {
9
+ /**
10
+ * @param {Array<String>} strings The static parts of the template
11
+ * @param {Array<*>} values The dynamic values of the template
12
+ */
13
+ constructor(strings, values) {
14
+ this.strings = strings;
15
+ this.values = values;
16
+ this[isHtmlTemplate] = true;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * A tagged template literal function that creates an `HtmlTemplate` instance.
22
+ * This function does not perform any parsing or string concatenation itself.
23
+ * It simply captures the template's parts for later processing.
4
24
  * @param {Array<String>} strings
5
25
  * @param {Array<*>} values
6
- * @returns {Object} A VDomNodeConfig object.
26
+ * @returns {Neo.functional.util.HtmlTemplate} An instance of HtmlTemplate
7
27
  */
8
28
  const html = (strings, ...values) => {
9
- let fullString = '';
10
- for (let i = 0; i < strings.length; i++) {
11
- fullString += strings[i];
12
- if (i < values.length) {
13
- // Use a unique placeholder for dynamic values
14
- fullString += `__DYNAMIC_VALUE_${i}__`;
15
- }
16
- }
17
-
18
- // Very basic parsing: find the first tag and its content
19
- const tagRegex = /<(\w+)([^>]*)>([\s\S]*?)<\/\1>/;
20
- const match = fullString.match(tagRegex);
21
-
22
- if (!match) {
23
- // If no matching tag, return a simple text node or empty div
24
- return { tag: 'div', text: fullString.replace(/__DYNAMIC_VALUE_\d+__/g, (m) => {
25
- const index = parseInt(m.match(/\d+/)[0]);
26
- return values[index];
27
- }) };
28
- }
29
-
30
- const rootTag = match[1];
31
- const attributesString = match[2];
32
- let innerContent = match[3];
33
-
34
- const vdomNode = {
35
- tag: rootTag,
36
- cn: []
37
- };
38
-
39
- // Parse attributes (very basic: only id for now)
40
- const idMatch = attributesString.match(/id="([^"]+)"/);
41
- if (idMatch) {
42
- vdomNode.id = idMatch[1];
43
- }
44
-
45
- // Replace dynamic placeholders with actual values in innerContent
46
- innerContent = innerContent.replace(/__DYNAMIC_VALUE_(\d+)__/g, (m, index) => {
47
- return values[parseInt(index)];
48
- });
49
-
50
- // For the current test case, we know it's <p> and <span>
51
- // This is still not a generic parser, but a step towards it.
52
- const pSpanRegex = /<p>([\s\S]*?)<\/p>\s*<span>([\s\S]*?)<\/span>/;
53
- const pSpanMatch = innerContent.match(pSpanRegex);
54
-
55
- if (pSpanMatch) {
56
- vdomNode.cn.push({
57
- tag: 'p',
58
- text: pSpanMatch[1]
59
- });
60
- vdomNode.cn.push({
61
- tag: 'span',
62
- text: pSpanMatch[2]
63
- });
64
- } else {
65
- // Fallback for simpler cases or if the regex doesn't match
66
- vdomNode.cn.push({
67
- tag: 'div', // Default child tag
68
- text: innerContent // Treat as plain text for now
69
- });
70
- }
71
-
72
- return vdomNode;
29
+ return new HtmlTemplate(strings, values);
73
30
  };
74
31
 
75
- export default html;
32
+ export { html, isHtmlTemplate, HtmlTemplate };
package/src/list/Base.mjs CHANGED
@@ -235,7 +235,7 @@ class List extends Component {
235
235
  * @protected
236
236
  */
237
237
  afterSetDisableSelection(value, oldValue) {
238
- value && this.rendered && this.selectionModel?.deselectAll()
238
+ value && this.vnodeInitialized && this.selectionModel?.deselectAll()
239
239
  }
240
240
 
241
241
  /**
@@ -796,7 +796,7 @@ class List extends Component {
796
796
  onStoreLoad() {
797
797
  let me = this;
798
798
 
799
- if (!me.mounted && me.rendering) {
799
+ if (!me.mounted && me.isVnodeInitializing) {
800
800
  me.on('mounted', () => {
801
801
  me.createItems()
802
802
  }, me, {once: true});
@@ -157,7 +157,7 @@ class Toast extends Manager {
157
157
  * @param {Neo.component.Toast} toast
158
158
  */
159
159
  showToast(toast) {
160
- toast.render(true);
160
+ toast.initVnode(true);
161
161
 
162
162
  let me = this;
163
163