neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5

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 (76) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/ViewportController.mjs +1 -1
  5. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  6. package/apps/portal/view/learn/MainContainerController.mjs +6 -6
  7. package/examples/button/effect/MainContainer.mjs +207 -0
  8. package/examples/button/effect/app.mjs +6 -0
  9. package/examples/button/effect/index.html +11 -0
  10. package/examples/button/effect/neo-config.json +6 -0
  11. package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
  12. package/learn/guides/datahandling/Grids.md +621 -0
  13. package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
  14. package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
  15. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  16. package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
  17. package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
  18. package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
  19. package/learn/tree.json +64 -57
  20. package/package.json +3 -3
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/Neo.mjs +244 -88
  23. package/src/button/Effect.mjs +435 -0
  24. package/src/collection/Base.mjs +35 -3
  25. package/src/component/Base.mjs +72 -61
  26. package/src/container/Base.mjs +28 -24
  27. package/src/controller/Base.mjs +87 -63
  28. package/src/core/Base.mjs +207 -33
  29. package/src/core/Compare.mjs +3 -13
  30. package/src/core/Config.mjs +230 -0
  31. package/src/core/ConfigSymbols.mjs +3 -0
  32. package/src/core/Effect.mjs +127 -0
  33. package/src/core/EffectBatchManager.mjs +68 -0
  34. package/src/core/EffectManager.mjs +38 -0
  35. package/src/core/Util.mjs +3 -18
  36. package/src/data/RecordFactory.mjs +22 -3
  37. package/src/grid/Container.mjs +8 -4
  38. package/src/grid/column/Component.mjs +1 -1
  39. package/src/state/Provider.mjs +343 -452
  40. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  41. package/src/tab/header/EffectButton.mjs +75 -0
  42. package/src/util/Function.mjs +52 -5
  43. package/src/vdom/Helper.mjs +9 -10
  44. package/src/vdom/VNode.mjs +1 -1
  45. package/src/worker/App.mjs +0 -5
  46. package/test/siesta/siesta.js +32 -0
  47. package/test/siesta/tests/CollectionBase.mjs +10 -10
  48. package/test/siesta/tests/VdomHelper.mjs +22 -59
  49. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  50. package/test/siesta/tests/config/Basic.mjs +149 -0
  51. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  52. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  53. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  54. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  55. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  56. package/test/siesta/tests/core/Effect.mjs +131 -0
  57. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  58. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  59. package/test/siesta/tests/state/Provider.mjs +537 -0
  60. package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
  61. package/learn/guides/ExtendingNeoClasses.md +0 -331
  62. /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
  63. /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
  64. /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
  65. /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
  66. /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
  67. /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
  68. /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
  69. /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
  70. /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
  71. /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
  72. /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
  73. /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
  74. /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
  75. /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
  76. /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
@@ -0,0 +1,41 @@
1
+ ## Neo.mjs v10.0.0-beta.4 Release Notes
2
+
3
+ This beta release introduces a powerful, opt-in **Reactive Config System**, a foundational architectural enhancement that enables fine-grained, cross-instance state management and significantly improves the developer experience for building complex, interactive applications.
4
+
5
+ ### Core Framework & Features
6
+
7
+ The centerpiece of this release is the new reactive config system, internally named "Config Atoms". This system is fully opt-in and backward compatible, ensuring that existing applications continue to work without any changes.
8
+
9
+ * **New Reactive Config System ("Config Atoms")**:
10
+ * **Opt-in Reactivity**: By adding a trailing underscore (`_`) to a config property in its `static config` block (e.g., `myValue_`), the property becomes "reactive." It is automatically wrapped in a `Neo.core.Config` instance, which acts as an observable container for its value.
11
+ * **Cross-Instance State Sharing**: The new system exposes a `subscribe()` method on each reactive config, allowing components to listen for changes to a config on any other component instance. This enables a clean, decoupled, and highly efficient way to implement cross-component state synchronization.
12
+ ```javascript
13
+ // In ComponentA
14
+ const componentB = Neo.get('b');
15
+
16
+ // Subscribe to changes in componentB's 'text' config
17
+ this.cleanup = componentB.getConfig('text').subscribe((newValue, oldValue) => {
18
+ this.someOtherProperty = newValue; // Reactively update
19
+ });
20
+
21
+ // In ComponentA's destroy() method:
22
+ this.cleanup?.(); // Simple, clean, and prevents memory leaks.
23
+ ```
24
+ * **Fine-Grained Control (Future Foundation)**: While not fully implemented in this beta, the groundwork is laid for using config descriptors to define custom `mergeStrategy` and `isEqual` functions, which will provide even more control over config behavior in a future release.
25
+ * **Transactional Consistency**: The new system fully integrates with Neo.mjs's existing transactional update mechanism (`set()`), ensuring that `afterSet` hooks always have a consistent view of all pending config changes within a single operation.
26
+
27
+ ### Developer Experience
28
+
29
+ * **Simplified State Management**: The `subscribe()` API drastically simplifies the logic required for components to react to state changes in other parts of the application, reducing boilerplate and the need for complex event chains.
30
+ * **Improved Debugging**: The observable nature of reactive configs makes it easier to trace how and when state changes occur throughout your application.
31
+
32
+ ---
33
+
34
+ This release marks a significant step forward in the evolution of the Neo.mjs architecture. The new reactive config system provides a robust foundation for building the next generation of highly interactive and state-driven web applications.
35
+
36
+ ### Call To Action
37
+
38
+ We are incredibly excited for you to start experimenting with this powerful new feature!
39
+
40
+ * **Try Reactive Configs**: Start by making a config reactive by adding a `_` to its name in the `static config` block and explore the new possibilities with `getConfig().subscribe()`.
41
+ * **Share Your Feedback**: This is a foundational change, and your feedback is more critical than ever. Please share your experiences, report any issues, or suggest improvements via [GitHub Issues](https://github.com/neomjs/neo/issues) or our [Slack Channel](https://join.slack.com/t/neomjs/shared_invite/zt-6c50ueeu-3E1~M4T9xkNnb~M_prEEOA).
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.0.0-beta.3'
23
+ * @member {String} version='10.0.0-beta.5'
24
24
  */
25
- version: '10.0.0-beta.3'
25
+ version: '10.0.0-beta.5'
26
26
  }
27
27
 
28
28
  /**
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-07-01",
19
+ "datePublished": "2025-07-09",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -50,7 +50,7 @@ class ViewportController extends Controller {
50
50
  '/examples/{itemId}': 'onExamplesRoute',
51
51
  '/home' : 'onHomeRoute',
52
52
  '/learn' : 'onLearnRoute',
53
- '/learn/{itemId}' : 'onLearnRoute',
53
+ '/learn/{*itemId}' : 'onLearnRoute',
54
54
  '/services' : 'onServicesRoute'
55
55
  },
56
56
  /**
@@ -107,7 +107,7 @@ class FooterContainer extends Container {
107
107
  }, {
108
108
  module: Component,
109
109
  cls : ['neo-version'],
110
- text : 'v10.0.0-beta.3'
110
+ text : 'v10.0.0-beta.5'
111
111
  }]
112
112
  }],
113
113
  /**
@@ -17,8 +17,8 @@ class MainContainerController extends Controller {
17
17
  * @member {Object} routes
18
18
  */
19
19
  routes: {
20
- '/learn' : 'onRouteDefault',
21
- '/learn/{itemId}': 'onRouteLearnItem'
20
+ '/learn' : 'onRouteDefault',
21
+ '/learn/{*itemId}': 'onRouteLearnItem'
22
22
  }
23
23
  }
24
24
 
@@ -118,7 +118,7 @@ class MainContainerController extends Controller {
118
118
  */
119
119
  onRouteDefault(data) {
120
120
  if (!this.getStateProvider().data.currentPageRecord) {
121
- this.onRouteLearnItem({itemId: 'benefits.Introduction'})
121
+ this.onRouteLearnItem({itemId: 'benefits/Introduction'})
122
122
  }
123
123
  }
124
124
 
@@ -126,15 +126,15 @@ class MainContainerController extends Controller {
126
126
  * @param {Object} data
127
127
  * @param {String} data.itemId
128
128
  */
129
- onRouteLearnItem(data) {
129
+ onRouteLearnItem({itemId}) {
130
130
  let stateProvider = this.getStateProvider(),
131
131
  store = stateProvider.getStore('contentTree');
132
132
 
133
133
  if (store.getCount() > 0) {
134
- stateProvider.data.currentPageRecord = store.get(data.itemId)
134
+ stateProvider.data.currentPageRecord = store.get(itemId)
135
135
  } else {
136
136
  store.on({
137
- load : () => {stateProvider.data.currentPageRecord = store.get(data.itemId)},
137
+ load : () => {stateProvider.data.currentPageRecord = store.get(itemId)},
138
138
  delay: 10,
139
139
  once : true
140
140
  })
@@ -0,0 +1,207 @@
1
+ import CheckBox from '../../../src/form/field/CheckBox.mjs';
2
+ import ComboBox from '../../../src/form/field/ComboBox.mjs';
3
+ import ConfigurationViewport from '../../ConfigurationViewport.mjs';
4
+ import EffectButton from '../../../src/button/Effect.mjs';
5
+ import NumberField from '../../../src/form/field/Number.mjs';
6
+ import Radio from '../../../src/form/field/Radio.mjs';
7
+ import TextField from '../../../src/form/field/Text.mjs';
8
+
9
+ /**
10
+ * @class Neo.examples.button.effect.MainContainer
11
+ * @extends Neo.examples.ConfigurationViewport
12
+ */
13
+ class MainContainer extends ConfigurationViewport {
14
+ static config = {
15
+ className : 'Neo.examples.button.effect.MainContainer',
16
+ configItemLabelWidth: 160,
17
+ configItemWidth : 280,
18
+ layout : {ntype: 'hbox', align: 'stretch'}
19
+ }
20
+
21
+ createConfigurationComponents() {
22
+ let me = this;
23
+
24
+ return [{
25
+ module : Radio,
26
+ checked : me.exampleComponent.badgePosition === 'bottom-left',
27
+ hideValueLabel: false,
28
+ labelText : 'badgePosition',
29
+ listeners : {change: me.onRadioChange.bind(me, 'badgePosition', 'bottom-left')},
30
+ name : 'badgePosition',
31
+ valueLabelText: 'bottom-left'
32
+ }, {
33
+ module : Radio,
34
+ checked : me.exampleComponent.badgePosition === 'bottom-right',
35
+ hideValueLabel: false,
36
+ labelText : '',
37
+ listeners : {change: me.onRadioChange.bind(me, 'badgePosition', 'bottom-right')},
38
+ name : 'badgePosition',
39
+ valueLabelText: 'bottom-right'
40
+ }, {
41
+ module : Radio,
42
+ checked : me.exampleComponent.badgePosition === 'top-left',
43
+ hideValueLabel: false,
44
+ labelText : '',
45
+ listeners : {change: me.onRadioChange.bind(me, 'badgePosition', 'top-left')},
46
+ name : 'badgePosition',
47
+ valueLabelText: 'top-left'
48
+ }, {
49
+ module : Radio,
50
+ checked : me.exampleComponent.badgePosition === 'top-right',
51
+ hideValueLabel: false,
52
+ labelText : '',
53
+ listeners : {change: me.onRadioChange.bind(me, 'badgePosition', 'top-right')},
54
+ name : 'badgePosition',
55
+ valueLabelText: 'top-right'
56
+ }, {
57
+ module : TextField,
58
+ labelText : 'badgeText',
59
+ listeners : {change: me.onConfigChange.bind(me, 'badgeText')},
60
+ style : {marginTop: '10px'},
61
+ value : me.exampleComponent.badgeText
62
+ }, {
63
+ module : CheckBox,
64
+ checked : me.exampleComponent.disabled,
65
+ labelText: 'disabled',
66
+ listeners: {change: me.onConfigChange.bind(me, 'disabled')},
67
+ style : {marginTop: '10px'}
68
+ }, {
69
+ module : NumberField,
70
+ clearable : true,
71
+ labelText : 'height',
72
+ listeners : {change: me.onConfigChange.bind(me, 'height')},
73
+ maxValue : 300,
74
+ minValue : 30,
75
+ stepSize : 5,
76
+ style : {marginTop: '10px'},
77
+ value : me.exampleComponent.height
78
+ }, {
79
+ module : TextField, // todo: selectField with options
80
+ labelText : 'iconCls',
81
+ listeners : {change: me.onConfigChange.bind(me, 'iconCls')},
82
+ value : me.exampleComponent.iconCls
83
+ }, {
84
+ module : TextField, // todo: colorPickerField
85
+ clearable : true,
86
+ labelText : 'iconColor',
87
+ listeners : {change: me.onConfigChange.bind(me, 'iconColor')},
88
+ value : me.exampleComponent.iconColor
89
+ }, {
90
+ module : Radio,
91
+ checked : me.exampleComponent.iconPosition === 'top',
92
+ hideValueLabel: false,
93
+ labelText : 'iconPosition',
94
+ listeners : {change: me.onRadioChange.bind(me, 'iconPosition', 'top')},
95
+ name : 'iconPosition',
96
+ style : {marginTop: '10px'},
97
+ valueLabelText: 'top'
98
+ }, {
99
+ module : Radio,
100
+ checked : me.exampleComponent.iconPosition === 'right',
101
+ hideValueLabel: false,
102
+ labelText : '',
103
+ listeners : {change: me.onRadioChange.bind(me, 'iconPosition', 'right')},
104
+ name : 'iconPosition',
105
+ valueLabelText: 'right'
106
+ }, {
107
+ module : Radio,
108
+ checked : me.exampleComponent.iconPosition === 'bottom',
109
+ hideValueLabel: false,
110
+ labelText : '',
111
+ listeners : {change: me.onRadioChange.bind(me, 'iconPosition', 'bottom')},
112
+ name : 'iconPosition',
113
+ valueLabelText: 'bottom'
114
+ }, {
115
+ module : Radio,
116
+ checked : me.exampleComponent.iconPosition === 'left',
117
+ hideValueLabel: false,
118
+ labelText : '',
119
+ listeners : {change: me.onRadioChange.bind(me, 'iconPosition', 'left')},
120
+ name : 'iconPosition',
121
+ valueLabelText: 'left'
122
+ }, {
123
+ module : NumberField,
124
+ clearable : true,
125
+ labelText : 'rippleEffectDuration',
126
+ listeners : {change: me.onConfigChange.bind(me, 'rippleEffectDuration')},
127
+ maxValue : 5000,
128
+ minValue : 100,
129
+ stepSize : 100,
130
+ style : {marginTop: '10px'},
131
+ value : me.exampleComponent.rippleEffectDuration
132
+ }, {
133
+ module : TextField,
134
+ clearable : true,
135
+ labelText : 'text',
136
+ listeners : {change: me.onConfigChange.bind(me, 'text')},
137
+ style : {marginTop: '10px'},
138
+ value : me.exampleComponent.text
139
+ }, {
140
+ module : TextField,
141
+ clearable : true,
142
+ labelText : 'tooltip',
143
+ listeners : {change: me.onConfigChange.bind(me, 'tooltip')},
144
+ style : {marginTop: '10px'},
145
+ value : me.exampleComponent.tooltip
146
+ }, {
147
+ module : ComboBox,
148
+ forceSelection: true,
149
+ labelText : 'ui',
150
+ listeners : {change: me.onConfigRecordChange.bind(me, 'ui')},
151
+ style : {marginTop: '10px'},
152
+ value : me.exampleComponent.ui,
153
+
154
+ store: {
155
+ data: [
156
+ {id: 'primary', name: 'primary'},
157
+ {id: 'secondary', name: 'secondary'},
158
+ {id: 'tertiary', name: 'tertiary'}
159
+ ]
160
+ }
161
+ }, {
162
+ module : CheckBox,
163
+ checked : me.exampleComponent.useRippleEffect,
164
+ labelText: 'useRippleEffect',
165
+ listeners: {change: me.onConfigChange.bind(me, 'useRippleEffect')},
166
+ style : {marginTop: '10px'}
167
+ }, {
168
+ module : NumberField,
169
+ clearable : true,
170
+ labelText : 'width',
171
+ listeners : {change: me.onConfigChange.bind(me, 'width')},
172
+ maxValue : 300,
173
+ minValue : 100,
174
+ stepSize : 5,
175
+ style : {marginTop: '10px'},
176
+ value : me.exampleComponent.width
177
+ }];
178
+ }
179
+
180
+ /**
181
+ * @returns {Neo.component.Base}
182
+ */
183
+ createExampleComponent() {
184
+ return Neo.create({
185
+ module : EffectButton,
186
+ badgeText: 'Badge',
187
+ flex : 'none',
188
+ handler : data => console.log('button click =>', data.component.id),
189
+ height : 50,
190
+ iconCls : 'fa fa-home',
191
+ style : {marginBottom: '1500px', marginTop: '500px'},
192
+ text : 'Hello World',
193
+ ui : 'primary',
194
+ width : 150
195
+ })
196
+ }
197
+
198
+ /**
199
+ * @param {String} config
200
+ * @param {Object} opts
201
+ */
202
+ onConfigRecordChange(config, opts) {
203
+ this.exampleComponent[config] = opts.value['id']
204
+ }
205
+ }
206
+
207
+ 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.button.effect'
6
+ });
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta charset="UTF-8">
6
+ <title>Neo EffectButton</title>
7
+ </head>
8
+ <body>
9
+ <script src="../../../src/MicroLoader.mjs" type="module"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "appPath" : "examples/button/effect/app.mjs",
3
+ "basePath" : "../../../",
4
+ "environment": "development",
5
+ "mainPath" : "./Main.mjs"
6
+ }
@@ -95,7 +95,7 @@ The `sourceId` concept provides an elegant solution:
95
95
  Set its `sourceId` to the ID of your primary store. This combobox store can then apply its own filters (e.g., based on
96
96
  user input in the combobox field) and sorters, completely independently of the grid store or the primary store.
97
97
 
98
- ```javascript
98
+ ```javascript readonly
99
99
  import Collection from '../../src/collection/Base.mjs';
100
100
  import Filter from '../../src/collection/Filter.mjs';
101
101
 
@@ -161,7 +161,7 @@ application to work with synchronized data while maintaining their own independe
161
161
 
162
162
  ### Example: Basic Collection Usage
163
163
 
164
- ```javascript
164
+ ```javascript readonly
165
165
  import Collection from '../../src/collection/Base.mjs';
166
166
 
167
167
  const myCollection = Neo.create(Collection, {
@@ -198,7 +198,7 @@ instances based on the store's model definition.
198
198
 
199
199
  ### Example: Adding and Removing
200
200
 
201
- ```javascript
201
+ ```javascript readonly
202
202
  import Collection from '../../src/collection/Base.mjs';
203
203
 
204
204
  const users = Neo.create(Collection, {
@@ -253,7 +253,7 @@ instances.**
253
253
 
254
254
  ### Example: Filtering a Collection
255
255
 
256
- ```javascript
256
+ ```javascript readonly
257
257
  import Collection from '../../src/collection/Base.mjs';
258
258
  import Filter from '../../src/collection/Filter.mjs';
259
259
 
@@ -323,7 +323,7 @@ instances.**
323
323
 
324
324
  ### Example: Sorting a Collection
325
325
 
326
- ```javascript
326
+ ```javascript readonly
327
327
  import Collection from '../../src/collection/Base.mjs';
328
328
  import Sorter from '../../src/collection/Sorter.mjs';
329
329
 
@@ -364,7 +364,7 @@ any operation that changes the collection's items.
364
364
 
365
365
  ### Example: Listening to Collection Events
366
366
 
367
- ```javascript
367
+ ```javascript readonly
368
368
  import Collection from '../../src/collection/Base.mjs';
369
369
 
370
370
  const tasks = Neo.create(Collection, {