neo.mjs 10.2.1 → 10.3.1

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 (80) 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 +71 -0
  4. package/.github/RELEASE_NOTES/v10.3.1.md +14 -0
  5. package/.github/epic-string-based-templates.md +690 -0
  6. package/ServiceWorker.mjs +2 -2
  7. package/apps/covid/view/MainContainer.mjs +1 -1
  8. package/apps/covid/view/country/Table.mjs +1 -1
  9. package/apps/portal/index.html +1 -1
  10. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  11. package/apps/portal/view/learn/ContentComponent.mjs +1 -1
  12. package/apps/realworld/api/Base.mjs +2 -2
  13. package/apps/sharedcovid/view/MainContainer.mjs +1 -1
  14. package/apps/sharedcovid/view/MainContainerController.mjs +1 -1
  15. package/buildScripts/buildAll.mjs +4 -0
  16. package/buildScripts/buildESModules.mjs +23 -75
  17. package/buildScripts/bundleParse5.mjs +27 -0
  18. package/buildScripts/util/astTemplateProcessor.mjs +210 -0
  19. package/buildScripts/util/templateBuildProcessor.mjs +331 -0
  20. package/buildScripts/webpack/development/webpack.config.appworker.mjs +11 -0
  21. package/buildScripts/webpack/loader/template-loader.mjs +21 -0
  22. package/buildScripts/webpack/production/webpack.config.appworker.mjs +11 -0
  23. package/examples/README.md +1 -1
  24. package/examples/component/wrapper/googleMaps/MarkerDialog.mjs +2 -2
  25. package/examples/form/field/email/MainContainer.mjs +0 -1
  26. package/examples/form/field/number/MainContainer.mjs +0 -1
  27. package/examples/form/field/picker/MainContainer.mjs +0 -1
  28. package/examples/form/field/time/MainContainer.mjs +0 -1
  29. package/examples/form/field/trigger/copyToClipboard/MainContainer.mjs +0 -1
  30. package/examples/form/field/url/MainContainer.mjs +0 -1
  31. package/examples/functional/nestedTemplateComponent/Component.mjs +100 -0
  32. package/examples/functional/nestedTemplateComponent/MainContainer.mjs +48 -0
  33. package/examples/functional/nestedTemplateComponent/app.mjs +6 -0
  34. package/examples/functional/nestedTemplateComponent/index.html +11 -0
  35. package/examples/functional/nestedTemplateComponent/neo-config.json +6 -0
  36. package/examples/functional/templateComponent/Component.mjs +61 -0
  37. package/examples/functional/templateComponent/MainContainer.mjs +48 -0
  38. package/examples/functional/templateComponent/app.mjs +6 -0
  39. package/examples/functional/templateComponent/index.html +11 -0
  40. package/examples/functional/templateComponent/neo-config.json +6 -0
  41. package/learn/gettingstarted/Setup.md +29 -12
  42. package/learn/guides/fundamentals/ApplicationBootstrap.md +2 -2
  43. package/learn/guides/fundamentals/InstanceLifecycle.md +5 -5
  44. package/learn/guides/uibuildingblocks/HtmlTemplates.md +191 -0
  45. package/learn/guides/uibuildingblocks/HtmlTemplatesUnderTheHood.md +156 -0
  46. package/learn/guides/uibuildingblocks/WorkingWithVDom.md +1 -1
  47. package/learn/tree.json +2 -0
  48. package/package.json +61 -56
  49. package/src/DefaultConfig.mjs +3 -3
  50. package/src/calendar/view/calendars/List.mjs +1 -1
  51. package/src/calendar/view/month/Component.mjs +1 -1
  52. package/src/calendar/view/week/Component.mjs +1 -1
  53. package/src/component/Abstract.mjs +1 -1
  54. package/src/component/Base.mjs +33 -27
  55. package/src/container/Base.mjs +5 -5
  56. package/src/controller/Application.mjs +5 -5
  57. package/src/dialog/Base.mjs +6 -6
  58. package/src/draggable/DragProxyComponent.mjs +4 -4
  59. package/src/form/field/ComboBox.mjs +1 -1
  60. package/src/functional/_export.mjs +2 -1
  61. package/src/functional/component/Base.mjs +142 -93
  62. package/src/functional/util/HtmlTemplateProcessor.mjs +243 -0
  63. package/src/functional/util/html.mjs +24 -67
  64. package/src/list/Base.mjs +2 -2
  65. package/src/manager/Toast.mjs +1 -1
  66. package/src/menu/List.mjs +1 -1
  67. package/src/mixin/VdomLifecycle.mjs +87 -90
  68. package/src/tab/Container.mjs +2 -2
  69. package/src/tooltip/Base.mjs +1 -1
  70. package/src/tree/Accordion.mjs +2 -2
  71. package/src/worker/App.mjs +7 -7
  72. package/test/components/files/component/Base.mjs +1 -1
  73. package/test/siesta/siesta.js +2 -0
  74. package/test/siesta/tests/classic/Button.mjs +5 -5
  75. package/test/siesta/tests/functional/Button.mjs +6 -6
  76. package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +193 -33
  77. package/test/siesta/tests/functional/Parse5Processor.mjs +82 -0
  78. package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +5 -5
  79. package/.github/epic-functional-components.md +0 -498
  80. package/.github/ticket-asymmetric-vdom-updates.md +0 -122
@@ -1,9 +1,10 @@
1
- import Neo from '../../../../src/Neo.mjs';
2
- import * as core from '../../../../src/core/_export.mjs';
3
- import FunctionalBase from '../../../../src/functional/component/Base.mjs';
4
- import DomApiVnodeCreator from '../../../../src/vdom/util/DomApiVnodeCreator.mjs';
5
- import VdomHelper from '../../../../src/vdom/Helper.mjs';
6
- import html from '../../../../src/functional/util/html.mjs';
1
+ import Neo from '../../../../src/Neo.mjs';
2
+ import * as core from '../../../../src/core/_export.mjs';
3
+ import HtmlTemplateProcessor from '../../../../src/functional/util/HtmlTemplateProcessor.mjs';
4
+ import FunctionalBase from '../../../../src/functional/component/Base.mjs';
5
+ import DomApiVnodeCreator from '../../../../src/vdom/util/DomApiVnodeCreator.mjs';
6
+ import VdomHelper from '../../../../src/vdom/Helper.mjs';
7
+ import {html} from '../../../../src/functional/util/html.mjs';
7
8
 
8
9
  // IMPORTANT: This test file uses real components and expects them to render.
9
10
  // We need to enable unitTestMode for isolation, but also allow VDOM updates.
@@ -16,10 +17,10 @@ Neo.config.useDomApiRenderer = true;
16
17
  const appName = 'HtmlTemplateTest';
17
18
  Neo.apps = Neo.apps || {};
18
19
  Neo.apps[appName] = {
19
- name : appName,
20
- fire : Neo.emptyFn,
21
- isMounted: () => true,
22
- rendering: false
20
+ name : appName,
21
+ fire : Neo.emptyFn,
22
+ isMounted : () => true,
23
+ vnodeInitialising: false
23
24
  };
24
25
 
25
26
  /**
@@ -33,7 +34,7 @@ class TestComponent extends FunctionalBase {
33
34
  testText_ : 'Hello from Template!'
34
35
  }
35
36
 
36
- createTemplateVdom(config) {
37
+ render(config) {
37
38
  return html`
38
39
  <div id="my-template-div">
39
40
  <p>${config.testText}</p>
@@ -45,48 +46,207 @@ class TestComponent extends FunctionalBase {
45
46
 
46
47
  TestComponent = Neo.setupClass(TestComponent);
47
48
 
49
+ /**
50
+ * @class TestComponentWithChildren
51
+ * @extends Neo.functional.component.Base
52
+ */
53
+ class TestComponentWithChildren extends FunctionalBase {
54
+ static config = {
55
+ className : 'TestComponentWithChildren',
56
+ enableHtmlTemplates: true,
57
+ childText_ : 'Inner Content'
58
+ }
59
+
60
+ render(config) {
61
+ return html`
62
+ <div id="parent-div">
63
+ <${TestComponent} id="child-comp" testText="${config.childText}" />
64
+ </div>
65
+ `;
66
+ }
67
+ }
68
+
69
+ TestComponentWithChildren = Neo.setupClass(TestComponentWithChildren);
70
+
71
+ /**
72
+ * @class TestConditionalComponent
73
+ * @extends Neo.functional.component.Base
74
+ */
75
+ class TestConditionalComponent extends FunctionalBase {
76
+ static config = {
77
+ className : 'TestConditionalComponent',
78
+ enableHtmlTemplates: true,
79
+ showDetails_ : false,
80
+ detailsText_ : 'Here are the details!'
81
+ }
82
+
83
+ render(config) {
84
+ return html`
85
+ <div id="conditional-div">
86
+ <h1>Title</h1>
87
+ ${config.showDetails && html`<p id="details-p">${config.detailsText}</p>`}
88
+ </div>
89
+ `;
90
+ }
91
+ }
92
+
93
+ TestConditionalComponent = Neo.setupClass(TestConditionalComponent);
94
+
48
95
 
49
96
  StartTest(t => {
50
- let component, vnode;
97
+ let component;
51
98
 
52
99
  t.beforeEach(async t => {
53
100
  component = Neo.create(TestComponent, {
54
101
  appName,
55
102
  id: 'my-test-component'
56
103
  });
57
-
58
- ({vnode} = await component.render());
104
+ // The initial initVnode() call is synchronous and returns the HtmlTemplate object.
105
+ // The actual VDOM is built asynchronously after this.
106
+ component.initVnode();
59
107
  component.mounted = true; // Manually mount to enable updates in the test env
60
108
  });
61
109
 
62
110
  t.afterEach(t => {
63
111
  component?.destroy();
64
112
  component = null;
65
- vnode = null;
66
113
  });
67
114
 
68
- t.it('should create initial vnode correctly using html template', async t => {
69
- t.expect(vnode.nodeName).toBe('div');
70
- t.expect(vnode.id).toBe('my-test-component'); // The component's own ID
71
- t.expect(vnode.childNodes.length).toBe(2);
115
+ t.it('should create initial vdom correctly using html template', async t => {
116
+ // Wait for the async VDOM update to complete
117
+ await t.waitFor(() => Object.keys(component.vdom).length > 0);
118
+
119
+ const vdom = component.vdom;
120
+
121
+ t.expect(vdom.id).toBe('my-test-component'); // The component's own ID
122
+ t.expect(vdom.tag).toBe('div');
123
+ t.expect(vdom.cn.length).toBe(2);
124
+
125
+ const pNode = vdom.cn[0];
126
+ t.expect(pNode.tag).toBe('p');
127
+ t.expect(pNode.text).toBe('Hello from Template!');
128
+
129
+ const spanNode = vdom.cn[1];
130
+ t.expect(spanNode.tag).toBe('span');
131
+ t.expect(spanNode.text).toBe('Another element');
132
+ });
133
+
134
+ t.it('should update vdom when reactive config changes', async t => {
135
+ // Wait for the initial render to finish
136
+ await t.waitFor(() => Object.keys(component.vdom).length > 0);
137
+
138
+ const initialVdom = {...component.vdom};
139
+
140
+ // Update the component
141
+ component.testText = 'Updated Text!';
142
+
143
+ // Wait for the async VDOM update to complete after the change
144
+ await t.waitFor(() => component.vdom.cn[0].text === 'Updated Text!');
145
+
146
+ const updatedVdom = component.vdom;
147
+
148
+ t.expect(updatedVdom.cn[0].text).toBe('Updated Text!');
149
+ // Ensure the rest of the VDOM structure remains the same
150
+ t.expect(updatedVdom.tag).toBe(initialVdom.tag);
151
+ t.expect(updatedVdom.cn.length).toBe(initialVdom.cn.length);
152
+ });
153
+
154
+ t.it('should handle nested components defined in a template', t => {
155
+ const parentComponent = Neo.create(TestComponentWithChildren, {
156
+ appName,
157
+ id: 'my-parent-component'
158
+ });
159
+
160
+ parentComponent.initVnode();
161
+ parentComponent.mounted = true;
162
+
163
+ const parentVdom = parentComponent.vdom;
164
+ t.expect(parentVdom.id).toBe('my-parent-component');
165
+ t.expect(parentVdom.tag).toBe('div');
166
+
167
+ // 1. Check that the parent's VDOM contains the correct reference
168
+ const childVdomRef = parentVdom.cn[0];
169
+ t.expect(childVdomRef.componentId).toBe('child-comp');
170
+
171
+ // 2. Get the child instance and check its properties
172
+ const childInstance = parentComponent.childComponents.get('child-comp').instance;
173
+
174
+ t.is(childInstance.constructor, TestComponent, 'Child instance should be an instance of TestComponent');
175
+
176
+ t.expect(childInstance.testText).toBe('Inner Content');
177
+
178
+ // 3. Check the child's own VDOM directly
179
+ const childVdom = childInstance.vdom;
180
+
181
+ t.expect(childVdom.tag).toBe('div');
182
+ t.expect(childVdom.cn[0].tag).toBe('p');
183
+ t.expect(childVdom.cn[0].text).toBe('Inner Content');
184
+
185
+ parentComponent.destroy();
186
+ });
187
+
188
+ t.it('should handle camelCase attributes correctly', async t => {
189
+ const parentComponent = Neo.create(TestComponentWithChildren, {
190
+ appName,
191
+ id: 'my-parent-component',
192
+ childText: 'Custom Text for camelCase'
193
+ });
194
+
195
+ parentComponent.initVnode();
196
+ parentComponent.mounted = true;
197
+
198
+ await t.waitFor(() => parentComponent.childComponents.get('child-comp'));
72
199
 
73
- const pNode = vnode.childNodes[0];
74
- t.expect(pNode.nodeName).toBe('p');
75
- t.expect(pNode.id).toContain('neo-vnode-'); // Expecting a generated ID
76
- t.expect(pNode.textContent).toBe('Hello from Template!');
200
+ const childInstance = parentComponent.childComponents.get('child-comp').instance;
77
201
 
78
- const spanNode = vnode.childNodes[1];
79
- t.expect(spanNode.nodeName).toBe('span');
80
- t.expect(spanNode.id).toContain('neo-vnode-'); // Expecting a generated ID
81
- t.expect(spanNode.textContent).toBe('Another element');
202
+ await t.waitFor(() => childInstance.vdom.cn?.[0]?.text === 'Custom Text for camelCase');
203
+
204
+ t.expect(childInstance.testText).toBe('Custom Text for camelCase');
205
+ t.expect(childInstance.vdom.cn[0].text).toBe('Custom Text for camelCase');
206
+
207
+ parentComponent.destroy();
82
208
  });
83
209
 
84
- t.it('should update vnode when reactive config changes', async t => {
85
- // Update the component to get the updated vnode
86
- const opts = await component.set({testText: 'Updated Text!'});
87
- vnode = opts.vnode;
210
+ t.it('should handle conditional rendering correctly', async t => {
211
+ const conditionalComponent = Neo.create(TestConditionalComponent, {
212
+ appName,
213
+ id: 'my-conditional-component'
214
+ });
215
+
216
+ conditionalComponent.initVnode();
217
+ conditionalComponent.mounted = true;
218
+
219
+ // 1. Initial state: details should NOT be rendered
220
+ await t.waitFor(() => conditionalComponent.vdom?.cn);
221
+
222
+ let vdom = conditionalComponent.vdom;
223
+ t.expect(vdom.cn.length).toBe(1); // Only the h1
224
+ t.expect(vdom.cn[0].tag).toBe('h1');
225
+ t.expect(vdom.cn.find(n => n && n.id === 'details-p')).toBeFalsy('Details <p> should not exist initially');
226
+
227
+ // 2. Update state: show the details
228
+ conditionalComponent.showDetails = true;
229
+
230
+ // 3. Wait for update and assert new state
231
+ await t.waitFor(() => conditionalComponent.vdom.cn.length > 1);
232
+
233
+ vdom = conditionalComponent.vdom;
234
+ t.expect(vdom.cn.length).toBe(2); // h1 and the new p
235
+ const detailsNode = vdom.cn.find(n => n.id === 'details-p');
236
+ t.ok(detailsNode, 'Details <p> should now exist');
237
+ t.expect(detailsNode.tag).toBe('p');
238
+ t.expect(detailsNode.text).toBe('Here are the details!');
239
+
240
+ // 4. Update state again: hide the details
241
+ conditionalComponent.showDetails = false;
242
+
243
+ // 5. Wait for update and assert final state
244
+ await t.waitFor(() => conditionalComponent.vdom.cn.length === 1);
245
+
246
+ vdom = conditionalComponent.vdom;
247
+ t.expect(vdom.cn.length).toBe(1); // Back to just the h1
248
+ t.expect(vdom.cn.find(n => n && n.id === 'details-p')).toBeFalsy('Details <p> should be removed');
88
249
 
89
- const pNode = vnode.childNodes[0];
90
- t.expect(pNode.textContent).toBe('Updated Text!');
250
+ conditionalComponent.destroy();
91
251
  });
92
252
  });
@@ -0,0 +1,82 @@
1
+ import Neo from '../../../../src/Neo.mjs';
2
+ import * as core from '../../../../src/core/_export.mjs';
3
+ import Button from '../../../../src/button/Base.mjs';
4
+ import HtmlTemplateProcessor from '../../../../src/functional/util/HtmlTemplateProcessor.mjs';
5
+ import {html} from '../../../../src/functional/util/html.mjs';
6
+
7
+ const processor = HtmlTemplateProcessor;
8
+
9
+ StartTest(async t => {
10
+ let parsedVdomResult;
11
+ const mockComponent = {
12
+ continueUpdateWithVdom: vdom => {
13
+ parsedVdomResult = vdom;
14
+ }
15
+ };
16
+
17
+ t.it('should parse a simple template with a single root node', async t => {
18
+ const template = html`<div><p>Hello</p></div>`;
19
+ await processor.process(template, mockComponent);
20
+
21
+ t.expect(parsedVdomResult).toEqual({
22
+ tag: 'div',
23
+ cn: [{
24
+ tag: 'p',
25
+ text: 'Hello'
26
+ }]
27
+ });
28
+ });
29
+
30
+ t.it('should handle interpolated values in text nodes', async t => {
31
+ const name = 'Neo';
32
+ const template = html`<p>Hello ${name}</p>`;
33
+ await processor.process(template, mockComponent);
34
+
35
+ t.expect(parsedVdomResult).toEqual({
36
+ tag: 'p',
37
+ text: 'Hello Neo'
38
+ });
39
+ });
40
+
41
+ t.it('should handle interpolated values in attributes', async t => {
42
+ const id = 'my-div';
43
+ const template = html`<div id="${id}"></div>`;
44
+ await processor.process(template, mockComponent);
45
+
46
+ t.expect(parsedVdomResult).toEqual({
47
+ tag: 'div',
48
+ id: 'my-div'
49
+ });
50
+ });
51
+
52
+ t.it('should handle non-string (object) values in attributes', async t => {
53
+ const styleObj = {color: 'blue', fontWeight: 'bold'};
54
+ const template = html`<div style="${styleObj}"></div>`;
55
+ await processor.process(template, mockComponent);
56
+
57
+ t.expect(parsedVdomResult).toEqual({
58
+ tag: 'div',
59
+ style: styleObj
60
+ });
61
+ });
62
+
63
+ t.it('should handle component tags via interpolation (lexical scope)', async t => {
64
+ const template = html`<${Button} text="Click Me"/>`;
65
+ await processor.process(template, mockComponent);
66
+
67
+ t.expect(parsedVdomResult).toEqual({
68
+ module: Button,
69
+ text: 'Click Me'
70
+ });
71
+ });
72
+
73
+ t.it('should handle component tags via global namespace string', async t => {
74
+ const template = html`<Neo.button.Base text="Global Click"/>`;
75
+ await processor.process(template, mockComponent);
76
+
77
+ t.expect(parsedVdomResult).toEqual({
78
+ module: Button,
79
+ text: 'Global Click'
80
+ });
81
+ });
82
+ });
@@ -16,10 +16,10 @@ Neo.config.useDomApiRenderer = true;
16
16
  const appName = 'VdomRealWorldTestApp';
17
17
  Neo.apps = Neo.apps || {};
18
18
  Neo.apps[appName] = {
19
- name : appName,
20
- fire : Neo.emptyFn,
21
- isMounted: () => true,
22
- rendering: false
19
+ name : appName,
20
+ fire : Neo.emptyFn,
21
+ isMounted : () => true,
22
+ vnodeInitialising: false
23
23
  };
24
24
 
25
25
  class TestGrandchild extends Component {
@@ -107,7 +107,7 @@ StartTest(t => {
107
107
  ]
108
108
  });
109
109
 
110
- await parent.render();
110
+ await parent.initVnode();
111
111
  child = parent.items[1]; // TestParent inserts a component at index 0
112
112
  grandchild = child.items[0];
113
113
  parent.mounted = true;