neo.mjs 10.1.1 → 10.2.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 (40) hide show
  1. package/.github/RELEASE_NOTES/v10.2.0.md +34 -0
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/covid/view/country/Gallery.mjs +1 -1
  4. package/apps/covid/view/country/Helix.mjs +1 -1
  5. package/apps/covid/view/country/Table.mjs +27 -29
  6. package/apps/portal/index.html +1 -1
  7. package/apps/portal/resources/data/blog.json +12 -0
  8. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  9. package/apps/sharedcovid/view/country/Gallery.mjs +1 -1
  10. package/apps/sharedcovid/view/country/Helix.mjs +1 -1
  11. package/apps/sharedcovid/view/country/Table.mjs +22 -22
  12. package/examples/grid/bigData/ControlsContainer.mjs +14 -0
  13. package/examples/stateProvider/inline/MainContainer.mjs +1 -1
  14. package/examples/stateProvider/twoWay/MainContainer.mjs +2 -2
  15. package/examples/treeAccordion/MainContainer.mjs +1 -1
  16. package/learn/blog/v10-deep-dive-functional-components.md +107 -97
  17. package/learn/blog/v10-deep-dive-reactivity.md +3 -3
  18. package/learn/blog/v10-deep-dive-state-provider.md +42 -137
  19. package/learn/blog/v10-deep-dive-vdom-revolution.md +35 -61
  20. package/learn/blog/v10-post1-love-story.md +3 -3
  21. package/learn/gettingstarted/DescribingTheUI.md +108 -33
  22. package/learn/guides/fundamentals/ConfigSystemDeepDive.md +118 -18
  23. package/learn/guides/fundamentals/InstanceLifecycle.md +121 -84
  24. package/learn/tree.json +1 -0
  25. package/learn/tutorials/CreatingAFunctionalButton.md +179 -0
  26. package/package.json +3 -3
  27. package/src/DefaultConfig.mjs +2 -2
  28. package/src/data/Store.mjs +8 -3
  29. package/src/date/SelectorContainer.mjs +2 -2
  30. package/src/form/field/Base.mjs +15 -1
  31. package/src/form/field/ComboBox.mjs +5 -15
  32. package/src/functional/component/Base.mjs +26 -0
  33. package/src/functional/util/html.mjs +75 -0
  34. package/src/state/Provider.mjs +7 -4
  35. package/src/tree/Accordion.mjs +1 -1
  36. package/test/siesta/siesta.js +8 -1
  37. package/test/siesta/tests/form/field/AfterSetValueSequence.mjs +106 -0
  38. package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +92 -0
  39. package/test/siesta/tests/state/FeedbackLoop.mjs +159 -0
  40. package/test/siesta/tests/state/Provider.mjs +56 -0
@@ -0,0 +1,34 @@
1
+ # Neo.mjs v10.2.0 Release Notes
2
+
3
+ ## Title
4
+ v10.2.0: Stabilization, DX Improvements & Template PoC
5
+
6
+ ## Introduction
7
+
8
+ Following the major architectural changes in v10.0.0 with the new core and state provider, v10.2.0 is focused on stabilization, refinement, and developer experience. This release adjusts our demonstration applications to the new architecture, fixes key regressions in the single and multi-window Covid apps, and polishes the Big Data Grid example.
9
+
10
+ A key highlight is the introduction of an early-stage proof-of-concept for string-based VDOM templates, offering a more intuitive way to build functional components.
11
+
12
+ ## Key Changes
13
+
14
+ ### New Features & Enhancements
15
+
16
+ - **PoC: String-Based VDOM Templates**: An initial proof-of-concept for using tagged template literals to define VDOM in functional components has been introduced. This feature can be enabled via the `enableHtmlTemplates` config and provides a more familiar, HTML-like syntax for building component views. See commit `a3bbca76653ffc822a102e9bde76dea3d8e7bb8b`.
17
+
18
+ - **Synchronous `fireChangeEvent`**: The `fireChangeEvent` method in `ComboBox` and other form fields is now fully synchronous. This resolves critical timing issues in applications like the Covid tracker, where dependent logic needs to execute immediately after a value changes to ensure state consistency. See issue [#7129](https://github.com/neomjs/neo/issues/7129).
19
+
20
+ - **Optimized ComboBox Selection**: The `afterSetValue` logic for single-select `ComboBox` fields has been optimized to prevent unnecessary processing when the selection has not changed, improving performance. See issue [#7125](https://github.com/neomjs/neo/issues/7125).
21
+
22
+ - **Refined StateProvider Logic**: The `StateProvider` change notification logic has been improved to be more precise, preventing unnecessary component updates and ensuring a more efficient data flow. See issue [#7124](https://github.com/neomjs/neo/issues/7124).
23
+
24
+ ### Bug Fixes
25
+
26
+ - **Form Field Value Sequencing**: Addressed a bug related to the sequencing of `afterSetValue` calls in form fields, ensuring that values are processed in the correct order. See issue [#7127](https://github.com/neomjs/neo/issues/7127).
27
+
28
+ - **Big Data Grid Example**: Fixed an issue in the Big Data Grid example where dropdowns in the controls container were not showing their initial values correctly. See issue [#7126](https://github.com/neomjs/neo/issues/7126).
29
+
30
+ - **Covid App Regression**: Fixed a regression bug in the Covid app that caused an infinite selection loop when choosing a country from the header dropdown. See issue [#7123](https://github.com/neomjs/neo/issues/7123).
31
+
32
+ - **SharedCovid App Regression**: Resolved a regression bug in the SharedCovid app where clicking on a table row would throw an error. See issue [#7122](https://github.com/neomjs/neo/issues/7122).
33
+
34
+ - **State Provider Feedback Loop**: Added a new test case to prevent feedback loops within the `StateProvider`, enhancing its reliability. See issue [#7128](https://github.com/neomjs/neo/issues/7128).
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.1.1'
23
+ * @member {String} version='10.2.0'
24
24
  */
25
- version: '10.1.1'
25
+ version: '10.2.0'
26
26
  }
27
27
 
28
28
  /**
@@ -21,7 +21,7 @@ class CountryGallery extends Gallery {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
26
  /**
27
27
  * @member {String|null} country_=null
@@ -21,7 +21,7 @@ class CountryHelix extends Helix {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
26
  /**
27
27
  * @member {String|null} country_=null
@@ -21,8 +21,18 @@ class Table extends Container {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
+ /**
27
+ * @member {String|null} country_=null
28
+ * @reactive
29
+ */
30
+ country_: null,
31
+ /**
32
+ * @member {Neo.data.Store} store=CountryStore
33
+ * @reactive
34
+ */
35
+ store: CountryStore,
26
36
  /**
27
37
  * Default configs for each column
28
38
  * @member {Object} columnDefaults
@@ -52,15 +62,15 @@ class Table extends Container {
52
62
  text : 'Country',
53
63
  width : 200,
54
64
 
55
- renderer: data => {
65
+ renderer(data) {
56
66
  return {
57
67
  cls : ['neo-country-column', 'neo-table-cell'],
58
68
  html: [
59
69
  '<div style="display: flex; align-items: center">',
60
- '<img style="height:20px; margin-right:10px; width:20px;" src="' + Util.getCountryFlagUrl(data.value) + '">' + data.value,
70
+ '<img style="height:20px; margin-right:10px; width:20px;" src="' + Util.getCountryFlagUrl(data.value) + '">' + data.value,
61
71
  '</div>'
62
72
  ].join('')
63
- };
73
+ }
64
74
  }
65
75
  }, {
66
76
  dataField: 'cases',
@@ -101,17 +111,7 @@ class Table extends Container {
101
111
  }, {
102
112
  dataField: 'testsPerOneMillion',
103
113
  text : 'Tests / 1M'
104
- }],
105
- /**
106
- * @member {String|null} country_=null
107
- * @reactive
108
- */
109
- country_: null,
110
- /**
111
- * @member {Neo.data.Store} store=CountryStore
112
- * @reactive
113
- */
114
- store: CountryStore
114
+ }]
115
115
  }
116
116
 
117
117
  /**
@@ -123,22 +123,20 @@ class Table extends Container {
123
123
  afterSetCountry(value, oldValue) {
124
124
  if (oldValue !== undefined) {
125
125
  let me = this,
126
- {view} = me,
127
- {selectionModel} = view,
126
+ {body} = me,
127
+ {selectionModel} = body,
128
128
  id;
129
129
 
130
- if (view) {
131
- if (value) {
132
- id = `${view.id}__tr__${value}`; // the store can not be loaded on the first selection
130
+ if (value) {
131
+ id = `${body.id}__tr__${value}`; // The store might not be loaded on the first selection
133
132
 
134
- if (!selectionModel.isSelected(id)) {
135
- selectionModel.select(id);
133
+ if (!selectionModel.isSelected(id)) {
134
+ selectionModel.select(id);
136
135
 
137
- me.mounted && Neo.main.DomAccess.scrollToTableRow({id: id});
138
- }
139
- } else {
140
- selectionModel.deselectAll();
136
+ me.mounted && Neo.main.DomAccess.scrollToTableRow({id: id})
141
137
  }
138
+ } else {
139
+ selectionModel.deselectAll()
142
140
  }
143
141
  }
144
142
  }
@@ -148,7 +146,7 @@ class Table extends Container {
148
146
  * @param {String[]} items
149
147
  */
150
148
  onDeselect(items) {
151
- this.country = null;
149
+ this.country = null
152
150
  }
153
151
 
154
152
  /**
@@ -161,12 +159,12 @@ class Table extends Container {
161
159
 
162
160
  if (me.store.getCount() > 0) {
163
161
  if (item) {
164
- item = me.view.getRecordByRowId(item)?.country;
162
+ item = me.body.getRecordByRowId(item)?.country
165
163
  }
166
164
 
167
165
  // in case getRecordByRowId() has no match, the initial row creation will include the selection
168
166
  if (item) {
169
- me.country = item;
167
+ me.country = item
170
168
  }
171
169
  }
172
170
  }
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-07-27",
19
+ "datePublished": "2025-07-30",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -1,4 +1,16 @@
1
1
  [{
2
+ "author" : "Tobias Uhlig",
3
+ "authorImage" : "author_TobiasUhlig.jpeg",
4
+ "date" : "Jul 28, 2025",
5
+ "id" : 67,
6
+ "image" : "DesigningFunctionalComponents.png",
7
+ "name" : "Designing Functional Components for a Multi-Threaded WorldDesigning Functional Components for a Multi-Threaded World",
8
+ "provider" : "Medium",
9
+ "publisher" : "ITNEXT",
10
+ "selectedInto": [],
11
+ "type" : "Blog Post",
12
+ "url" : "https://itnext.io/designing-functional-components-for-a-multi-threaded-world-22f27119509a?source=friends_link&sk=5434dd074a70c23a9ce0e8c78a98fed8"
13
+ }, {
2
14
  "author" : "Tobias Uhlig",
3
15
  "authorImage" : "author_TobiasUhlig.jpeg",
4
16
  "date" : "Jul 24, 2025",
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.1.1'
111
+ text : 'v10.2.0'
112
112
  }]
113
113
  }],
114
114
  /**
@@ -21,7 +21,7 @@ class CountryGallery extends Gallery {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
26
  /**
27
27
  * @member {String|null} country_=null
@@ -21,7 +21,7 @@ class CountryHelix extends Helix {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
26
  /**
27
27
  * @member {String|null} country_=null
@@ -21,8 +21,18 @@ class Table extends Container {
21
21
  * @member {Object} bind
22
22
  */
23
23
  bind: {
24
- country: {twoWay: true, value: data => data.country}
24
+ country: {key: 'country', twoWay: true}
25
25
  },
26
+ /**
27
+ * @member {String|null} country_=null
28
+ * @reactive
29
+ */
30
+ country_: null,
31
+ /**
32
+ * @member {Neo.data.Store} store=CountryStore
33
+ * @reactive
34
+ */
35
+ store: CountryStore,
26
36
  /**
27
37
  * Default configs for each column
28
38
  * @member {Object} columnDefaults
@@ -52,7 +62,7 @@ class Table extends Container {
52
62
  text : 'Country',
53
63
  width : 200,
54
64
 
55
- renderer: function(data) {
65
+ renderer(data) {
56
66
  return {
57
67
  cls : ['neo-country-column', 'neo-table-cell'],
58
68
  html: [
@@ -60,7 +70,7 @@ class Table extends Container {
60
70
  '<img style="height:20px; margin-right:10px; width:20px;" src="' + Util.getCountryFlagUrl(data.value) + '">' + data.value,
61
71
  '</div>'
62
72
  ].join('')
63
- };
73
+ }
64
74
  }
65
75
  }, {
66
76
  dataField: 'cases',
@@ -101,17 +111,7 @@ class Table extends Container {
101
111
  }, {
102
112
  dataField: 'testsPerOneMillion',
103
113
  text : 'Tests / 1M'
104
- }],
105
- /**
106
- * @member {String|null} country_=null
107
- * @reactive
108
- */
109
- country_: null,
110
- /**
111
- * @member {Neo.data.Store} store=CountryStore
112
- * @reactive
113
- */
114
- store: CountryStore
114
+ }]
115
115
  }
116
116
 
117
117
  /**
@@ -123,20 +123,20 @@ class Table extends Container {
123
123
  afterSetCountry(value, oldValue) {
124
124
  if (oldValue !== undefined) {
125
125
  let me = this,
126
- {view} = me,
127
- {selectionModel} = view,
126
+ {body} = me,
127
+ {selectionModel} = body,
128
128
  id;
129
129
 
130
130
  if (value) {
131
- id = `${me.view.id}__tr__${value}`; // the store can not be loaded on the first selection
131
+ id = `${body.id}__tr__${value}`; // The store might not be loaded on the first selection
132
132
 
133
133
  if (!selectionModel.isSelected(id)) {
134
134
  selectionModel.select(id);
135
135
 
136
- me.mounted && Neo.main.DomAccess.scrollToTableRow({id: id});
136
+ me.mounted && Neo.main.DomAccess.scrollToTableRow({id: id})
137
137
  }
138
138
  } else {
139
- selectionModel.deselectAll();
139
+ selectionModel.deselectAll()
140
140
  }
141
141
  }
142
142
  }
@@ -146,7 +146,7 @@ class Table extends Container {
146
146
  * @param {String[]} items
147
147
  */
148
148
  onDeselect(items) {
149
- this.country = null;
149
+ this.country = null
150
150
  }
151
151
 
152
152
  /**
@@ -159,12 +159,12 @@ class Table extends Container {
159
159
 
160
160
  if (me.store.getCount() > 0) {
161
161
  if (item) {
162
- item = me.view.getRecordByRowId(item)?.country;
162
+ item = me.body.getRecordByRowId(item)?.country
163
163
  }
164
164
 
165
165
  // in case getRecordByRowId() has no match, the initial row creation will include the selection
166
166
  if (item) {
167
- me.country = item;
167
+ me.country = item
168
168
  }
169
169
  }
170
170
  }
@@ -9,6 +9,20 @@ import TabContainer from '../../../src/tab/Container.mjs';
9
9
  * @extends Neo.container.Base
10
10
  */
11
11
  class ControlsContainer extends Container {
12
+ /**
13
+ * We need to add a 1-frame delay here. Rationale: the change events trigger huge computational logic.
14
+ * We need to ensure that a ComboBox list selection first updates the field input value node,
15
+ * and then continues with the grid update, to maintain a great UX.
16
+ * @member {Object} delayable
17
+ * @static
18
+ */
19
+ static delayable = {
20
+ onAmountColumnsChange : {type: 'buffer', timer: 30},
21
+ onAmountRowsChange : {type: 'buffer', timer: 30},
22
+ onBufferColumnRangeChange: {type: 'buffer', timer: 30},
23
+ onBufferRowRangeChange : {type: 'buffer', timer: 30}
24
+ }
25
+
12
26
  static config = {
13
27
  /**
14
28
  * @member {String} className='Neo.examples.grid.bigData.ControlsContainer'
@@ -88,7 +88,7 @@ class MainContainer extends Viewport {
88
88
  width : 300,
89
89
 
90
90
  bind: {
91
- value: {twoWay: true, value: data => data.button1Text}
91
+ value: {key: 'button1Text', twoWay: true}
92
92
  }
93
93
  }, {
94
94
  module : TextField,
@@ -49,7 +49,7 @@ class MainContainer extends Viewport {
49
49
  width : 300,
50
50
 
51
51
  bind: {
52
- value: {twoWay: true, value: data => data.user.details.firstname}
52
+ value: {key: 'user.details.firstname', twoWay: true}
53
53
  },
54
54
  }, {
55
55
  module : TextField,
@@ -59,7 +59,7 @@ class MainContainer extends Viewport {
59
59
  width : 300,
60
60
 
61
61
  bind: {
62
- value: {twoWay: true, value: data => data.user.details.lastname}
62
+ value: {key: 'user.details.lastname', twoWay: true}
63
63
  },
64
64
  }],
65
65
  /**
@@ -113,7 +113,7 @@ class MainContainer extends ConfigurationViewport {
113
113
  items : [{
114
114
  module: AccordionTree,
115
115
 
116
- bind: {selection: {twoWay: true, value: data => data.selection}},
116
+ bind: {selection: {key: 'selection', twoWay: true}},
117
117
 
118
118
  store: store,
119
119