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
@@ -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
  };
@@ -1,7 +1,7 @@
1
1
  # Client Requirements
2
2
 
3
3
  Running the examples locally works fine in all environments inside all major browsers at this point:
4
- Chromium, Safari & Firefox
4
+ Chrome, Edge, Firefox & Safari
5
5
 
6
6
 
7
7
  # Local Web-Server Requirements
@@ -71,8 +71,8 @@ class MarkerDialog extends DialogBase {
71
71
  return `${day}. ${month} <b>${year}</b> ${hour}:${minute}`
72
72
  }
73
73
 
74
- async onRender(data, automount) {
75
- super.onRender(data, automount)
74
+ async onInitVnode(data, automount) {
75
+ super.onInitVnode(data, automount)
76
76
 
77
77
  let me = this;
78
78
 
@@ -118,7 +118,6 @@ class MainContainer extends ConfigurationViewport {
118
118
 
119
119
  createExampleComponent() {
120
120
  return Neo.create(EmailField, {
121
- autoRender: false,
122
121
  clearable : true,
123
122
  labelText : 'Label',
124
123
  labelWidth: 70,
@@ -114,7 +114,6 @@ class MainContainer extends ConfigurationViewport {
114
114
 
115
115
  createExampleComponent() {
116
116
  return Neo.create(NumberField, {
117
- autoRender : false,
118
117
  clearToOriginalValue: true,
119
118
  labelText : 'Label',
120
119
  labelWidth : 70,
@@ -110,7 +110,6 @@ class MainContainer extends ConfigurationViewport {
110
110
 
111
111
  createExampleComponent() {
112
112
  return Neo.create(PickerField, {
113
- autoRender : false,
114
113
  clearToOriginalValue: true,
115
114
  labelText : 'Label',
116
115
  labelWidth : 70,
@@ -136,7 +136,6 @@ class MainContainer extends ConfigurationViewport {
136
136
 
137
137
  createExampleComponent() {
138
138
  return Neo.create(TimeField, {
139
- autoRender : false,
140
139
  clearable : true,
141
140
  labelPosition: 'inline',
142
141
  labelText : 'Pick a time',
@@ -106,7 +106,6 @@ class MainContainer extends ConfigurationViewport {
106
106
 
107
107
  createExampleComponent() {
108
108
  return Neo.create(TextField, {
109
- autoRender : false,
110
109
  clearable : true,
111
110
  showOnHover: true,
112
111
  labelText : 'Label',
@@ -118,7 +118,6 @@ class MainContainer extends ConfigurationViewport {
118
118
 
119
119
  createExampleComponent() {
120
120
  return Neo.create(UrlField, {
121
- autoRender : false,
122
121
  clearable : true,
123
122
  labelText : 'Label',
124
123
  labelWidth : 70,
@@ -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);
@@ -0,0 +1,6 @@
1
+ import MainContainer from './MainContainer.mjs';
2
+
3
+ export const onStart = () => Neo.app({
4
+ mainView: MainContainer,
5
+ name : 'Neo.examples.functional.nestedTemplateComponent'
6
+ });