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,9 +1,10 @@
|
|
1
|
-
import Neo
|
2
|
-
import * as core
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
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
|
20
|
-
fire
|
21
|
-
isMounted: () => true,
|
22
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
69
|
-
|
70
|
-
t.
|
71
|
-
|
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
|
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
|
-
|
79
|
-
|
80
|
-
t.expect(
|
81
|
-
t.expect(
|
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
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
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
|
20
|
-
fire
|
21
|
-
isMounted: () => true,
|
22
|
-
|
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.
|
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;
|