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
@@ -0,0 +1,331 @@
|
|
1
|
+
import { HtmlTemplate } from '../../src/functional/util/html.mjs';
|
2
|
+
import * as parse5 from '../../dist/parse5.mjs'; // parse5 is bundled and available
|
3
|
+
|
4
|
+
/**
|
5
|
+
* This script contains the core logic for Neo.mjs's build-time processing of HTML tagged template literals.
|
6
|
+
* Its primary purpose is to convert the `html`...` syntax into a standard, serializable Neo.mjs VDOM
|
7
|
+
* object. This transformation happens during the build process (`build-dist-esm`), meaning the code that
|
8
|
+
* runs in the browser receives a pre-compiled, optimized VDOM structure, eliminating the need for a
|
9
|
+
* client-side HTML parser and improving runtime performance.
|
10
|
+
*
|
11
|
+
* The process is carefully designed to be a "compile-time equivalent" of the client-side parser, ensuring
|
12
|
+
* that developers get a consistent experience between development and production modes.
|
13
|
+
*
|
14
|
+
* The core challenge is to take the raw strings and an array of code strings (representing the dynamic
|
15
|
+
* parts like `${this.name}`) and correctly reconstruct a VDOM tree. This involves:
|
16
|
+
* 1. Flattening nested templates.
|
17
|
+
* 2. Using `parse5` to create a standard HTML AST.
|
18
|
+
* 3. Walking the AST and converting each node into a VDOM object.
|
19
|
+
* 4. Crucially, preserving the JavaScript expressions as-is for runtime evaluation, which is achieved by
|
20
|
+
* wrapping them in special placeholders (`##__NEO_EXPR__...##`) that are later converted back into
|
21
|
+
* raw code in the final AST.
|
22
|
+
* 5. Handling mixed content (e.g., `Hello, ${this.name}!`) by converting it into a robust chain of
|
23
|
+
* string concatenations rather than a fragile template literal reconstruction.
|
24
|
+
*/
|
25
|
+
|
26
|
+
// Defining regexes at the module level is a performance best practice,
|
27
|
+
// as it prevents them from being re-created on every function call.
|
28
|
+
const
|
29
|
+
/**
|
30
|
+
* @private
|
31
|
+
* @const {RegExp} regexAttribute
|
32
|
+
* Finds an attribute name right before an interpolated value (e.g., `... style=${...}`).
|
33
|
+
* This is crucial for preserving the original mixed-case spelling of attributes when they are dynamic.
|
34
|
+
*/
|
35
|
+
regexAttribute = /\s+([a-zA-Z][^=]*)\s*=\s*"?$/,
|
36
|
+
/**
|
37
|
+
* @private
|
38
|
+
* @const {RegExp} regexDynamicValue
|
39
|
+
* Finds a placeholder for a dynamic value that is the entire attribute value.
|
40
|
+
*/
|
41
|
+
regexDynamicValue = /^__DYNAMIC_VALUE_(\d+)__$/,
|
42
|
+
/**
|
43
|
+
* @private
|
44
|
+
* @const {RegExp} regexDynamicValueG
|
45
|
+
* Finds all dynamic value placeholders within a string (globally).
|
46
|
+
*/
|
47
|
+
regexDynamicValueG = /__DYNAMIC_VALUE_(\d+)__/g,
|
48
|
+
/**
|
49
|
+
* @private
|
50
|
+
* @const {RegExp} regexNested
|
51
|
+
* Finds placeholders for nested templates or dynamic component tags to re-index them during flattening.
|
52
|
+
*/
|
53
|
+
regexNested = /(__DYNAMIC_VALUE_|neotag)(\d+)/g,
|
54
|
+
/**
|
55
|
+
* @private
|
56
|
+
* @const {RegExp} regexOriginalTagName
|
57
|
+
* Extracts the original tag name from its source code string to preserve case sensitivity, as parse5 lowercases all tags.
|
58
|
+
*/
|
59
|
+
regexOriginalTagName = /<([\w\.]+)/,
|
60
|
+
/**
|
61
|
+
* @private
|
62
|
+
* @const {RegExp} selfClosingComponentRegex
|
63
|
+
* Finds self-closing custom component tags (e.g., `<MyComponent />`) and converts them to
|
64
|
+
* explicit start/end tags (`<MyComponent></MyComponent>`) because `parse5` in fragment mode
|
65
|
+
* does not correctly handle them for non-standard elements.
|
66
|
+
*/
|
67
|
+
selfClosingComponentRegex = /<((?:[A-Z][\w\.]*)|(?:neotag\d+))([^>]*?)\/?>/g;
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Recursively converts a single parse5 AST node into a Neo.mjs VDOM node.
|
71
|
+
* This is the heart of the transformation process, translating the HTML structure into a JSON structure.
|
72
|
+
* @param {object} node The parse5 AST node to process.
|
73
|
+
* @param {string[]} values The array of placeholder strings for interpolated values (e.g., '##__NEO_EXPR__...##').
|
74
|
+
* @param {string} originalString The flattened, raw template string.
|
75
|
+
* @param {object} attributeNameMap A map of dynamic value indices to their original, case-sensitive attribute names.
|
76
|
+
* @param {object} options Configuration options for parsing.
|
77
|
+
* @param {object} parseState A state object to track parsing progress (currently unused but available).
|
78
|
+
* @returns {object|string|null} A VDOM node, a raw expression placeholder string, or null if the node is empty.
|
79
|
+
* @private
|
80
|
+
*/
|
81
|
+
function convertNodeToVdom(node, values, originalString, attributeNameMap, options, parseState) {
|
82
|
+
// 1. Handle text nodes
|
83
|
+
if (node.nodeName === '#text') {
|
84
|
+
let text = node.value;
|
85
|
+
|
86
|
+
if (options?.trimWhitespace) {
|
87
|
+
text = text.replace(/\s+/g, ' ').trim();
|
88
|
+
}
|
89
|
+
|
90
|
+
if (text === '') return null;
|
91
|
+
|
92
|
+
// CRITICAL: This handles conditional rendering (e.g., `${condition && template}`).
|
93
|
+
// If a text node consists of a SINGLE dynamic value, we must return the raw placeholder string.
|
94
|
+
// This allows the expression to be placed directly into the parent's `cn` array, where it will be
|
95
|
+
// evaluated at runtime. If we wrapped it in a `{vtype: 'text', ...}` object, it would always render as a string.
|
96
|
+
const singleDynamicMatch = text.match(/^__DYNAMIC_VALUE_(\d+)__$/);
|
97
|
+
if (singleDynamicMatch) {
|
98
|
+
const index = parseInt(singleDynamicMatch[1], 10);
|
99
|
+
return values[index]; // Return the raw '##__NEO_EXPR__...##' placeholder
|
100
|
+
}
|
101
|
+
|
102
|
+
// If the text node is a mix of strings and dynamic values (e.g., `Hello, ${name}`),
|
103
|
+
// we build a robust string concatenation chain (e.g., `'Hello, ' + (name)`). This is more
|
104
|
+
// reliable than trying to reconstruct a nested template literal, as it avoids complex escaping issues.
|
105
|
+
const regex = /(__DYNAMIC_VALUE_\d+__)/g;
|
106
|
+
const parts = text.split(regex).filter(p => p);
|
107
|
+
|
108
|
+
if (parts.length > 1) {
|
109
|
+
const additionChain = parts.map(part => {
|
110
|
+
const match = part.match(/__DYNAMIC_VALUE_(\d+)__/);
|
111
|
+
if (match) {
|
112
|
+
const index = parseInt(match[1], 10);
|
113
|
+
const value = values[index]; // This is the '##__NEO_EXPR__...' string
|
114
|
+
const exprMatch = value.match(/##__NEO_EXPR__(.*)##__NEO_EXPR__##/s);
|
115
|
+
if (exprMatch) {
|
116
|
+
return `(${exprMatch[1]})`; // Wrap expression in parens for safety
|
117
|
+
}
|
118
|
+
}
|
119
|
+
// Escape single quotes for the string literal part of the chain.
|
120
|
+
return `'${part.replace(/'/g, "\'" )}'`;
|
121
|
+
}).filter(p => p !== `''`).join(' + ');
|
122
|
+
|
123
|
+
// The entire chain becomes a single expression placeholder.
|
124
|
+
return { vtype: 'text', text: `##__NEO_EXPR__${additionChain}##__NEO_EXPR__##` };
|
125
|
+
}
|
126
|
+
|
127
|
+
// It's a simple static text node.
|
128
|
+
return { vtype: 'text', text };
|
129
|
+
}
|
130
|
+
|
131
|
+
// 2. Handle element nodes
|
132
|
+
if (node.nodeName && node.sourceCodeLocation?.startTag) {
|
133
|
+
const vdom = {};
|
134
|
+
const tagName = node.tagName;
|
135
|
+
|
136
|
+
// A `neotag` is a placeholder for a dynamically injected component constructor (e.g., `<${Button}>`).
|
137
|
+
if (tagName.startsWith('neotag')) {
|
138
|
+
const index = parseInt(tagName.replace('neotag', ''), 10);
|
139
|
+
vdom.module = values[index];
|
140
|
+
} else {
|
141
|
+
// parse5 lowercases all tag names. To support case-sensitive component tags (e.g., `<MyComponent>`),
|
142
|
+
// we must retrieve the original tag name from the source string.
|
143
|
+
const { startTag } = node.sourceCodeLocation;
|
144
|
+
const startTagStr = originalString.substring(startTag.startOffset, startTag.endOffset);
|
145
|
+
const originalTagNameMatch = startTagStr.match(regexOriginalTagName);
|
146
|
+
if (originalTagNameMatch) {
|
147
|
+
const originalTagName = originalTagNameMatch[1];
|
148
|
+
// By convention, a tag starting with an uppercase letter is a Neo.mjs component.
|
149
|
+
if (originalTagName[0] === originalTagName[0].toUpperCase()) {
|
150
|
+
// At build time, we don't resolve the component. We create a placeholder that the
|
151
|
+
// main build script will convert into a plain Identifier in the AST.
|
152
|
+
vdom.module = { __neo_component_name__: originalTagName };
|
153
|
+
} else {
|
154
|
+
vdom.tag = originalTagName;
|
155
|
+
}
|
156
|
+
} else {
|
157
|
+
vdom.tag = tagName; // Fallback
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
// Re-construct attributes, re-inserting dynamic values and preserving original case.
|
162
|
+
node.attrs?.forEach(attr => {
|
163
|
+
const match = attr.value.match(regexDynamicValue);
|
164
|
+
// If the entire attribute is a dynamic value, we can directly assign the placeholder.
|
165
|
+
if (match) {
|
166
|
+
const dynamicValueIndex = parseInt(match[1], 10);
|
167
|
+
const attrName = attributeNameMap[dynamicValueIndex] || attr.name;
|
168
|
+
vdom[attrName] = values[dynamicValueIndex];
|
169
|
+
} else {
|
170
|
+
// If the attribute is a mix of strings and dynamic values, build a concatenation chain.
|
171
|
+
let hasDynamicPart = false;
|
172
|
+
const valueParts = attr.value.split(regexDynamicValueG).map(part => {
|
173
|
+
if (part.match(/^\d+$/)) {
|
174
|
+
const index = parseInt(part, 10);
|
175
|
+
if (values[index]) {
|
176
|
+
hasDynamicPart = true;
|
177
|
+
const value = values[index];
|
178
|
+
const exprMatch = value.match(/##__NEO_EXPR__(.*)##__NEO_EXPR__##/s);
|
179
|
+
return exprMatch ? `(${exprMatch[1]})` : JSON.stringify(value);
|
180
|
+
}
|
181
|
+
}
|
182
|
+
return `'${part.replace(/'/g, "\'" )}'`;
|
183
|
+
});
|
184
|
+
|
185
|
+
if (hasDynamicPart) {
|
186
|
+
const finalExpression = valueParts.filter(p => p !== `''`).join(' + ');
|
187
|
+
vdom[attr.name] = `##__NEO_EXPR__${finalExpression}##__NEO_EXPR__##`;
|
188
|
+
} else {
|
189
|
+
vdom[attr.name] = attr.value;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
});
|
193
|
+
|
194
|
+
// Recursively process child nodes.
|
195
|
+
if (node.childNodes?.length > 0) {
|
196
|
+
const children = node.childNodes.map(child => convertNodeToVdom(child, values, originalString, attributeNameMap, options, parseState)).filter(Boolean);
|
197
|
+
if (children.length > 0) {
|
198
|
+
// Optimization: If a node has only one child, check if it's a text node (either static or dynamic)
|
199
|
+
// and simplify the VDOM by moving the content directly into the parent's `text` property.
|
200
|
+
if (children.length === 1) {
|
201
|
+
const child = children[0];
|
202
|
+
if (child.vtype === 'text') {
|
203
|
+
vdom.text = child.text;
|
204
|
+
} else if (typeof child === 'string' && child.startsWith('##__NEO_EXPR__')) {
|
205
|
+
vdom.text = child; // Assign the dynamic expression placeholder directly.
|
206
|
+
} else {
|
207
|
+
vdom.cn = children;
|
208
|
+
}
|
209
|
+
} else {
|
210
|
+
vdom.cn = children;
|
211
|
+
}
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
return vdom;
|
216
|
+
}
|
217
|
+
|
218
|
+
return null;
|
219
|
+
}
|
220
|
+
|
221
|
+
/**
|
222
|
+
* Kicks off the AST to VDOM conversion for the entire template.
|
223
|
+
* @param {object} ast The root parse5 AST.
|
224
|
+
* @param {string[]} values The array of placeholder strings for interpolated values.
|
225
|
+
* @param {string} originalString The flattened, raw template string.
|
226
|
+
* @param {object} attributeNameMap A map of dynamic value indices to their original, case-sensitive attribute names.
|
227
|
+
* @param {object} options Configuration options for parsing.
|
228
|
+
* @param {object} parseState A state object to track parsing progress.
|
229
|
+
* @returns {object} The final Neo.mjs VDOM.
|
230
|
+
* @private
|
231
|
+
*/
|
232
|
+
function convertAstToVdom(ast, values, originalString, attributeNameMap, options, parseState) {
|
233
|
+
if (!ast.childNodes || ast.childNodes.length < 1) {
|
234
|
+
return {};
|
235
|
+
}
|
236
|
+
|
237
|
+
const children = ast.childNodes.map(child => convertNodeToVdom(child, values, originalString, attributeNameMap, options, parseState)).filter(Boolean);
|
238
|
+
|
239
|
+
// If the template has only one root node, we return it directly.
|
240
|
+
// Otherwise, we return a fragment-like object with children in a `cn` array.
|
241
|
+
if (children.length === 1) {
|
242
|
+
return children[0];
|
243
|
+
}
|
244
|
+
|
245
|
+
return { cn: children };
|
246
|
+
}
|
247
|
+
|
248
|
+
/**
|
249
|
+
* Flattens a potentially nested HtmlTemplate object into a single string and a corresponding array of values.
|
250
|
+
* This is a necessary pre-processing step before parsing with `parse5`, which only accepts a single string.
|
251
|
+
* It recursively walks through nested templates, merging their strings and values, and carefully re-indexes
|
252
|
+
* all placeholders to be unique within the final flattened structure.
|
253
|
+
* @param {Neo.functional.util.HtmlTemplate} template The root template object.
|
254
|
+
* @param {string[]} [parentValues] Used for recursion.
|
255
|
+
* @param {object} [parentAttributeMap] Used for recursion.
|
256
|
+
* @returns {{flatString: string, flatValues: Array<*>, attributeNameMap: Object}}
|
257
|
+
* @private
|
258
|
+
*/
|
259
|
+
function flattenTemplate(template, parentValues, parentAttributeMap) {
|
260
|
+
let flatString = '';
|
261
|
+
const flatValues = parentValues || [];
|
262
|
+
const attributeNameMap = parentAttributeMap || {};
|
263
|
+
|
264
|
+
for (let i = 0; i < template.strings.length; i++) {
|
265
|
+
let str = template.strings[i];
|
266
|
+
const attrMatch = str.match(regexAttribute);
|
267
|
+
|
268
|
+
flatString += str;
|
269
|
+
|
270
|
+
if (i < template.values.length) {
|
271
|
+
const value = template.values[i];
|
272
|
+
|
273
|
+
if (value instanceof HtmlTemplate) {
|
274
|
+
// A value can be another template. Recurse into it.
|
275
|
+
const nestedStartIndex = flatValues.length;
|
276
|
+
const nested = flattenTemplate(value, flatValues, attributeNameMap);
|
277
|
+
// The nested template's placeholders must be re-indexed to fit into the parent's value array.
|
278
|
+
const nestedString = nested.flatString.replace(regexNested, (match, p1, p2) => {
|
279
|
+
return `${p1}${parseInt(p2, 10) + nestedStartIndex}`;
|
280
|
+
});
|
281
|
+
flatString += nestedString;
|
282
|
+
// flatValues and attributeNameMap are mutated by the recursive call, so no merge needed here.
|
283
|
+
} else if (value !== false && value != null) {
|
284
|
+
// Falsy values are ignored, enabling conditional rendering.
|
285
|
+
const currentIndex = flatValues.length;
|
286
|
+
if (attrMatch) {
|
287
|
+
// If the value is for an attribute, store the original attribute name.
|
288
|
+
attributeNameMap[currentIndex] = attrMatch[1];
|
289
|
+
}
|
290
|
+
|
291
|
+
// Replace the dynamic value with a placeholder.
|
292
|
+
if (template.strings[i].trim().endsWith('<') || template.strings[i].trim().endsWith('</')) {
|
293
|
+
flatString += `neotag${currentIndex}`;
|
294
|
+
} else {
|
295
|
+
flatString += `__DYNAMIC_VALUE_${currentIndex}__`;
|
296
|
+
}
|
297
|
+
flatValues.push(value);
|
298
|
+
}
|
299
|
+
}
|
300
|
+
}
|
301
|
+
|
302
|
+
return { flatString, flatValues, attributeNameMap };
|
303
|
+
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* The main entry point for the build-time template processor.
|
307
|
+
* It orchestrates the flattening, parsing, and VDOM conversion.
|
308
|
+
* @param {string[]} strings The static string parts of the template literal from the AST.
|
309
|
+
* @param {string[]} expressionCodeStrings The raw JavaScript code strings for the dynamic parts from the AST.
|
310
|
+
* @returns {object} The resulting serializable JSON VDOM object.
|
311
|
+
*/
|
312
|
+
export function processHtmlTemplateLiteral(strings, expressionCodeStrings) {
|
313
|
+
// At build time, we don't evaluate expressions. We wrap the raw code strings in placeholders.
|
314
|
+
// These placeholders will be converted back into real AST nodes by the main build script.
|
315
|
+
const values = expressionCodeStrings.map(exprCode => `##__NEO_EXPR__${exprCode}##__NEO_EXPR__##`);
|
316
|
+
|
317
|
+
// The HtmlTemplate class provides a convenient structure for handling template parts.
|
318
|
+
const htmlTemplateInstance = new HtmlTemplate(strings, values);
|
319
|
+
|
320
|
+
// 1. Flatten the template to handle nested templates.
|
321
|
+
const { flatString, flatValues, attributeNameMap } = flattenTemplate(htmlTemplateInstance);
|
322
|
+
// 2. Fix self-closing tags for parse5 compatibility.
|
323
|
+
const stringWithClosingTags = flatString.replace(selfClosingComponentRegex, '<$1$2></$1>');
|
324
|
+
// 3. Parse the flattened string into an AST.
|
325
|
+
const ast = parse5.parseFragment(stringWithClosingTags, { sourceCodeLocationInfo: true });
|
326
|
+
const parseState = { attrNameIndex: 0 };
|
327
|
+
// 4. Convert the AST into the final VDOM object.
|
328
|
+
const parsedVdom = convertAstToVdom(ast, flatValues, stringWithClosingTags, attributeNameMap, { trimWhitespace: true }, parseState);
|
329
|
+
|
330
|
+
return parsedVdom;
|
331
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
/**
|
2
|
+
* A regex to check if a string is a valid JavaScript identifier.
|
3
|
+
* @member {RegExp} validIdentifierRegex=/^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
4
|
+
*/
|
5
|
+
const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Serializes a VDOM object into a JavaScript object literal string.
|
9
|
+
* This is a custom implementation to ensure keys are unquoted if they are
|
10
|
+
* valid identifiers, and correctly quoted otherwise. It also handles
|
11
|
+
* special placeholders for runtime expressions.
|
12
|
+
* @param {Object} vdom The VDOM object to serialize.
|
13
|
+
* @returns {String} The string representation of the VDOM.
|
14
|
+
*/
|
15
|
+
export function vdomToString(vdom) {
|
16
|
+
if (vdom === null) {
|
17
|
+
return 'null';
|
18
|
+
}
|
19
|
+
if (typeof vdom !== 'object') {
|
20
|
+
// It's a primitive value (string, number, boolean)
|
21
|
+
// Check for our special expression placeholder
|
22
|
+
if (typeof vdom === 'string') {
|
23
|
+
const match = vdom.match(/##__NEO_EXPR__(.*)##__NEO_EXPR__##/);
|
24
|
+
if (match) {
|
25
|
+
return match[1]; // Return the raw expression
|
26
|
+
}
|
27
|
+
}
|
28
|
+
// Otherwise, stringify it normally
|
29
|
+
return JSON.stringify(vdom);
|
30
|
+
}
|
31
|
+
|
32
|
+
if (Array.isArray(vdom)) {
|
33
|
+
return `[${vdom.map(vdomToString).join(',')}]`;
|
34
|
+
}
|
35
|
+
|
36
|
+
const parts = [];
|
37
|
+
for (const key in vdom) {
|
38
|
+
if (Object.prototype.hasOwnProperty.call(vdom, key)) {
|
39
|
+
const value = vdom[key];
|
40
|
+
const keyString = validIdentifierRegex.test(key) ? key : `'${key}'`;
|
41
|
+
parts.push(`${keyString}:${vdomToString(value)}`);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
return `{${parts.join(',')}}`;
|
46
|
+
}
|
@@ -162,6 +162,17 @@ export default env => {
|
|
162
162
|
chunkFilename: 'chunks/app/[id].js',
|
163
163
|
filename : filenameConfig.workers.app.output,
|
164
164
|
path : path.resolve(cwd, buildTarget.folder)
|
165
|
+
},
|
166
|
+
|
167
|
+
module: {
|
168
|
+
rules: [
|
169
|
+
{
|
170
|
+
test: /\.mjs$/,
|
171
|
+
use: [{
|
172
|
+
loader: path.resolve(neoPath, 'buildScripts/webpack/loader/template-loader.mjs')
|
173
|
+
}]
|
174
|
+
}
|
175
|
+
]
|
165
176
|
}
|
166
177
|
}
|
167
178
|
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { processFileContent } from '../../util/astTemplateProcessor.mjs';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* This Webpack loader is responsible for applying the AST-based `html` template
|
5
|
+
* transformation to `.mjs` files during the Webpack build process.
|
6
|
+
*
|
7
|
+
* It acts as a pre-processor for JavaScript files before they are handled by other
|
8
|
+
* loaders or bundled by Webpack.
|
9
|
+
*
|
10
|
+
* @param {string} source The source code of the file being processed.
|
11
|
+
* @returns {string} The transformed source code.
|
12
|
+
*/
|
13
|
+
export default function(source) {
|
14
|
+
// `this.resourcePath` is a property provided by Webpack's loader context,
|
15
|
+
// giving us the absolute path to the file being processed. This is crucial
|
16
|
+
// for logging meaningful errors.
|
17
|
+
const result = processFileContent(source, this.resourcePath);
|
18
|
+
|
19
|
+
// Return the (potentially modified) content to the next loader in the chain.
|
20
|
+
return result.content;
|
21
|
+
};
|
@@ -169,6 +169,17 @@ export default async function(env) {
|
|
169
169
|
chunkFilename: 'chunks/app/[id].js',
|
170
170
|
filename : filenameConfig.workers.app.output,
|
171
171
|
path : path.resolve(cwd, buildTarget.folder)
|
172
|
+
},
|
173
|
+
|
174
|
+
module: {
|
175
|
+
rules: [
|
176
|
+
{
|
177
|
+
test: /\.mjs$/,
|
178
|
+
use: [{
|
179
|
+
loader: path.resolve(neoPath, 'buildScripts/webpack/loader/template-loader.mjs')
|
180
|
+
}]
|
181
|
+
}
|
182
|
+
]
|
172
183
|
}
|
173
184
|
}
|
174
185
|
};
|
package/examples/README.md
CHANGED
@@ -71,8 +71,8 @@ class MarkerDialog extends DialogBase {
|
|
71
71
|
return `${day}. ${month} <b>${year}</b> ${hour}:${minute}`
|
72
72
|
}
|
73
73
|
|
74
|
-
async
|
75
|
-
super.
|
74
|
+
async onInitVnode(data, automount) {
|
75
|
+
super.onInitVnode(data, automount)
|
76
76
|
|
77
77
|
let me = this;
|
78
78
|
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import Button from '../../../src/button/Base.mjs';
|
2
|
+
import {defineComponent, html, useConfig, useEvent} from '../../../src/functional/_export.mjs';
|
3
|
+
|
4
|
+
export default defineComponent({
|
5
|
+
config: {
|
6
|
+
/**
|
7
|
+
* @member {String} className='Neo.examples.functional.nestedTemplateComponent.Component'
|
8
|
+
*/
|
9
|
+
className: 'Neo.examples.functional.nestedTemplateComponent.Component',
|
10
|
+
/**
|
11
|
+
* @member {String} detailsText_='Here are some more details!'
|
12
|
+
* @reactive
|
13
|
+
*/
|
14
|
+
detailsText_: 'Here are some more details!',
|
15
|
+
/**
|
16
|
+
* This is the key to unlock the `Template literals` based syntax for VDOM.
|
17
|
+
* @member {Boolean} enableHtmlTemplates=true
|
18
|
+
* @reactive
|
19
|
+
*/
|
20
|
+
enableHtmlTemplates: true,
|
21
|
+
/**
|
22
|
+
* @member {String} greeting_='Hello'
|
23
|
+
* @reactive
|
24
|
+
*/
|
25
|
+
greeting_: 'Hello',
|
26
|
+
/**
|
27
|
+
* @member {String} jobTitle_='Neo.mjs Developer'
|
28
|
+
* @reactive
|
29
|
+
*/
|
30
|
+
jobTitle_: 'Neo.mjs Developer'
|
31
|
+
},
|
32
|
+
|
33
|
+
render(config) {
|
34
|
+
const [isActive, setIsActive] = useConfig(true);
|
35
|
+
const [showDetails, setShowDetails] = useConfig(false);
|
36
|
+
|
37
|
+
// This event listener is for the main container to toggle the active state
|
38
|
+
useEvent('click', (event) => {
|
39
|
+
// Stop the event from bubbling up to avoid toggling the active state
|
40
|
+
// when the button is clicked. The button has its own handler.
|
41
|
+
if (event.target.id === 'details-button') {
|
42
|
+
event.stopPropagation();
|
43
|
+
} else {
|
44
|
+
setIsActive(prev => !prev);
|
45
|
+
}
|
46
|
+
});
|
47
|
+
|
48
|
+
// The idiomatic way to handle a button click is with the handler config.
|
49
|
+
const onButtonClick = () => {
|
50
|
+
setShowDetails(prev => !prev);
|
51
|
+
};
|
52
|
+
|
53
|
+
const cardStyle = {
|
54
|
+
border : '1px solid #eee',
|
55
|
+
borderRadius: '8px',
|
56
|
+
boxShadow : '0 2px 4px rgba(0,0,0,0.1)',
|
57
|
+
cursor : 'pointer',
|
58
|
+
padding : '16px',
|
59
|
+
textAlign : 'center'
|
60
|
+
};
|
61
|
+
|
62
|
+
const statusStyle = {
|
63
|
+
backgroundColor: isActive ? '#28a745' : '#dc3545',
|
64
|
+
borderRadius : '12px',
|
65
|
+
color : 'white',
|
66
|
+
display : 'inline-block',
|
67
|
+
fontSize : '12px',
|
68
|
+
marginTop : '10px',
|
69
|
+
padding : '4px 8px'
|
70
|
+
};
|
71
|
+
|
72
|
+
// 1. Nested Template: A separate template for the details section
|
73
|
+
const detailsTemplate = html`
|
74
|
+
<div style="margin-top: 15px; padding: 10px; border-top: 1px solid #eee;">
|
75
|
+
<p>${config.detailsText}</p>
|
76
|
+
</div>
|
77
|
+
`;
|
78
|
+
|
79
|
+
return html`
|
80
|
+
<div style=${cardStyle}>
|
81
|
+
<h2>${config.greeting}, Neo!</h2>
|
82
|
+
<p>${config.jobTitle}</p>
|
83
|
+
|
84
|
+
<!-- 3. Component via Tag Name, using the idiomatic handler config -->
|
85
|
+
<${Button}
|
86
|
+
handler=${onButtonClick}
|
87
|
+
id="details-button"
|
88
|
+
text="${showDetails ? 'Hide' : 'Show'} Details"
|
89
|
+
/>
|
90
|
+
|
91
|
+
<!-- 2. Conditional Rendering: Using a boolean to show the nested template -->
|
92
|
+
${showDetails && detailsTemplate}
|
93
|
+
|
94
|
+
<div style="${statusStyle}">
|
95
|
+
${isActive ? 'Active' : 'Inactive'}
|
96
|
+
</div>
|
97
|
+
</div>
|
98
|
+
`
|
99
|
+
}
|
100
|
+
});
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import ConfigurationViewport from '../../ConfigurationViewport.mjs';
|
2
|
+
import MyFunctionalComponent from './Component.mjs';
|
3
|
+
import TextField from '../../../src/form/field/Text.mjs';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* @class Neo.examples.functional.nestedTemplateComponent.MainContainer
|
7
|
+
* @extends Neo.examples.ConfigurationViewport
|
8
|
+
*/
|
9
|
+
class MainContainer extends ConfigurationViewport {
|
10
|
+
static config = {
|
11
|
+
className : 'Neo.examples.functional.nestedTemplateComponent.MainContainer',
|
12
|
+
configItemLabelWidth: 160,
|
13
|
+
configItemWidth : 280,
|
14
|
+
layout : {ntype: 'hbox', align: 'stretch'}
|
15
|
+
}
|
16
|
+
|
17
|
+
createConfigurationComponents() {
|
18
|
+
let me = this;
|
19
|
+
|
20
|
+
return [{
|
21
|
+
module : TextField,
|
22
|
+
clearable : true,
|
23
|
+
labelText : 'greeting',
|
24
|
+
listeners : {change: me.onConfigChange.bind(me, 'greeting')},
|
25
|
+
style : {marginTop: '10px'},
|
26
|
+
value : me.exampleComponent.greeting
|
27
|
+
}, {
|
28
|
+
module : TextField,
|
29
|
+
clearable : true,
|
30
|
+
labelText : 'jobTitle',
|
31
|
+
listeners : {change: me.onConfigChange.bind(me, 'jobTitle')},
|
32
|
+
style : {marginTop: '10px'},
|
33
|
+
value : me.exampleComponent.jobTitle
|
34
|
+
}]
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* @returns {Neo.functional.Component}
|
39
|
+
*/
|
40
|
+
createExampleComponent() {
|
41
|
+
return {
|
42
|
+
module : MyFunctionalComponent,
|
43
|
+
greeting: 'Hi'
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
export default Neo.setupClass(MainContainer);
|