neo.mjs 10.2.0 → 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.
- package/.github/CONCEPT.md +2 -4
- package/.github/GETTING_STARTED.md +72 -51
- package/.github/RELEASE_NOTES/v10.2.1.md +17 -0
- package/.github/RELEASE_NOTES/v10.3.0.md +54 -0
- package/.github/epic-string-based-templates.md +690 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/covid/view/GalleryContainer.mjs +1 -1
- package/apps/covid/view/HelixContainer.mjs +1 -1
- package/apps/covid/view/MainContainer.mjs +1 -1
- package/apps/covid/view/WorldMapContainer.mjs +4 -4
- package/apps/covid/view/country/Table.mjs +1 -1
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/ContentComponent.mjs +1 -1
- package/apps/realworld/api/Base.mjs +2 -2
- package/apps/sharedcovid/view/GalleryContainer.mjs +1 -1
- package/apps/sharedcovid/view/HelixContainer.mjs +1 -1
- package/apps/sharedcovid/view/MainContainer.mjs +1 -1
- package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
- package/apps/sharedcovid/view/WorldMapContainer.mjs +4 -4
- package/buildScripts/buildESModules.mjs +23 -75
- package/buildScripts/bundleParse5.mjs +27 -0
- package/buildScripts/util/astTemplateProcessor.mjs +210 -0
- package/buildScripts/util/templateBuildProcessor.mjs +331 -0
- package/buildScripts/util/vdomToString.mjs +46 -0
- package/buildScripts/webpack/development/webpack.config.appworker.mjs +11 -0
- package/buildScripts/webpack/loader/template-loader.mjs +21 -0
- package/buildScripts/webpack/production/webpack.config.appworker.mjs +11 -0
- package/examples/README.md +1 -1
- package/examples/component/wrapper/googleMaps/MarkerDialog.mjs +2 -2
- package/examples/form/field/email/MainContainer.mjs +0 -1
- package/examples/form/field/number/MainContainer.mjs +0 -1
- package/examples/form/field/picker/MainContainer.mjs +0 -1
- package/examples/form/field/time/MainContainer.mjs +0 -1
- package/examples/form/field/trigger/copyToClipboard/MainContainer.mjs +0 -1
- package/examples/form/field/url/MainContainer.mjs +0 -1
- package/examples/functional/nestedTemplateComponent/Component.mjs +100 -0
- package/examples/functional/nestedTemplateComponent/MainContainer.mjs +48 -0
- package/examples/functional/nestedTemplateComponent/app.mjs +6 -0
- package/examples/functional/nestedTemplateComponent/index.html +11 -0
- package/examples/functional/nestedTemplateComponent/neo-config.json +6 -0
- package/examples/functional/templateComponent/Component.mjs +61 -0
- package/examples/functional/templateComponent/MainContainer.mjs +48 -0
- package/examples/functional/templateComponent/app.mjs +6 -0
- package/examples/functional/templateComponent/index.html +11 -0
- package/examples/functional/templateComponent/neo-config.json +6 -0
- package/learn/gettingstarted/Setup.md +29 -12
- package/learn/guides/fundamentals/ApplicationBootstrap.md +2 -2
- package/learn/guides/fundamentals/InstanceLifecycle.md +5 -5
- package/learn/guides/uibuildingblocks/HtmlTemplates.md +191 -0
- package/learn/guides/uibuildingblocks/HtmlTemplatesUnderTheHood.md +156 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +1 -1
- package/learn/tree.json +2 -0
- package/package.json +62 -56
- package/src/DefaultConfig.mjs +3 -3
- package/src/button/Base.mjs +13 -4
- package/src/calendar/view/calendars/List.mjs +1 -1
- package/src/calendar/view/month/Component.mjs +1 -1
- package/src/calendar/view/week/Component.mjs +1 -1
- package/src/component/Abstract.mjs +1 -1
- package/src/component/Base.mjs +33 -27
- package/src/container/Base.mjs +14 -7
- package/src/controller/Application.mjs +5 -5
- package/src/dialog/Base.mjs +6 -6
- package/src/draggable/DragProxyComponent.mjs +4 -4
- package/src/form/field/ComboBox.mjs +1 -1
- package/src/functional/_export.mjs +2 -1
- package/src/functional/component/Base.mjs +142 -93
- package/src/functional/util/HtmlTemplateProcessor.mjs +243 -0
- package/src/functional/util/html.mjs +24 -67
- package/src/list/Base.mjs +2 -2
- package/src/manager/Toast.mjs +1 -1
- package/src/menu/List.mjs +1 -1
- package/src/mixin/VdomLifecycle.mjs +87 -90
- package/src/tab/Container.mjs +2 -2
- package/src/tooltip/Base.mjs +1 -1
- package/src/tree/Accordion.mjs +2 -2
- package/src/worker/App.mjs +7 -7
- package/test/components/files/component/Base.mjs +1 -1
- package/test/siesta/siesta.js +2 -0
- package/test/siesta/tests/classic/Button.mjs +5 -5
- package/test/siesta/tests/functional/Button.mjs +6 -6
- package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +193 -33
- package/test/siesta/tests/functional/Parse5Processor.mjs +82 -0
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +5 -5
- package/.github/epic-functional-components.md +0 -498
- package/.github/ticket-asymmetric-vdom-updates.md +0 -122
@@ -1,6 +1,7 @@
|
|
1
|
-
import Abstract
|
2
|
-
import Effect
|
3
|
-
import NeoArray
|
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
|
99
|
+
* Triggered after the isReady config got changed
|
89
100
|
* @param {Boolean} value
|
90
101
|
* @param {Boolean} oldValue
|
91
102
|
* @protected
|
92
103
|
*/
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if (oldValue !== undefined) {
|
97
|
-
const me = this;
|
104
|
+
afterSetIsReady(value, oldValue) {
|
105
|
+
const me = this;
|
98
106
|
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
114
|
+
* Triggered after the mounted config got changed
|
108
115
|
* @param {Boolean} value
|
109
116
|
* @param {Boolean} oldValue
|
110
117
|
* @protected
|
111
118
|
*/
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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.
|
202
|
-
return me.
|
292
|
+
if (me.enableHtmlTemplates && typeof me.render === 'function') {
|
293
|
+
return me.render(config)
|
203
294
|
}
|
204
|
-
|
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
|
-
//
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
303
|
-
|
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
|
-
|
378
|
-
|
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
|
-
|
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 {
|
26
|
+
* @returns {Neo.functional.util.HtmlTemplate} An instance of HtmlTemplate
|
7
27
|
*/
|
8
28
|
const html = (strings, ...values) => {
|
9
|
-
|
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
|
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.
|
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.
|
799
|
+
if (!me.mounted && me.isVnodeInitializing) {
|
800
800
|
me.on('mounted', () => {
|
801
801
|
me.createItems()
|
802
802
|
}, me, {once: true});
|