neo.mjs 10.1.1 → 10.2.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 (49) hide show
  1. package/.github/RELEASE_NOTES/v10.2.0.md +34 -0
  2. package/.github/RELEASE_NOTES/v10.2.1.md +17 -0
  3. package/ServiceWorker.mjs +2 -2
  4. package/apps/covid/view/GalleryContainer.mjs +1 -1
  5. package/apps/covid/view/HelixContainer.mjs +1 -1
  6. package/apps/covid/view/WorldMapContainer.mjs +4 -4
  7. package/apps/covid/view/country/Gallery.mjs +1 -1
  8. package/apps/covid/view/country/Helix.mjs +1 -1
  9. package/apps/covid/view/country/Table.mjs +27 -29
  10. package/apps/portal/index.html +1 -1
  11. package/apps/portal/resources/data/blog.json +12 -0
  12. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  13. package/apps/sharedcovid/view/GalleryContainer.mjs +1 -1
  14. package/apps/sharedcovid/view/HelixContainer.mjs +1 -1
  15. package/apps/sharedcovid/view/WorldMapContainer.mjs +4 -4
  16. package/apps/sharedcovid/view/country/Gallery.mjs +1 -1
  17. package/apps/sharedcovid/view/country/Helix.mjs +1 -1
  18. package/apps/sharedcovid/view/country/Table.mjs +22 -22
  19. package/examples/grid/bigData/ControlsContainer.mjs +14 -0
  20. package/examples/stateProvider/inline/MainContainer.mjs +1 -1
  21. package/examples/stateProvider/twoWay/MainContainer.mjs +2 -2
  22. package/examples/treeAccordion/MainContainer.mjs +1 -1
  23. package/learn/blog/v10-deep-dive-functional-components.md +107 -97
  24. package/learn/blog/v10-deep-dive-reactivity.md +3 -3
  25. package/learn/blog/v10-deep-dive-state-provider.md +42 -137
  26. package/learn/blog/v10-deep-dive-vdom-revolution.md +35 -61
  27. package/learn/blog/v10-post1-love-story.md +3 -3
  28. package/learn/gettingstarted/DescribingTheUI.md +108 -33
  29. package/learn/guides/fundamentals/ConfigSystemDeepDive.md +118 -18
  30. package/learn/guides/fundamentals/InstanceLifecycle.md +121 -84
  31. package/learn/tree.json +1 -0
  32. package/learn/tutorials/CreatingAFunctionalButton.md +179 -0
  33. package/package.json +3 -3
  34. package/src/DefaultConfig.mjs +2 -2
  35. package/src/button/Base.mjs +13 -4
  36. package/src/container/Base.mjs +9 -2
  37. package/src/data/Store.mjs +8 -3
  38. package/src/date/SelectorContainer.mjs +2 -2
  39. package/src/form/field/Base.mjs +15 -1
  40. package/src/form/field/ComboBox.mjs +5 -15
  41. package/src/functional/component/Base.mjs +26 -0
  42. package/src/functional/util/html.mjs +75 -0
  43. package/src/state/Provider.mjs +7 -4
  44. package/src/tree/Accordion.mjs +1 -1
  45. package/test/siesta/siesta.js +8 -1
  46. package/test/siesta/tests/form/field/AfterSetValueSequence.mjs +106 -0
  47. package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +92 -0
  48. package/test/siesta/tests/state/FeedbackLoop.mjs +159 -0
  49. package/test/siesta/tests/state/Provider.mjs +56 -0
@@ -2,10 +2,15 @@
2
2
 
3
3
  Understanding the lifecycle of a class instance in Neo.mjs is crucial for building robust and predictable
4
4
  applications. The framework provides a series of well-defined hooks that allow you to tap into different stages of an
5
- instance's life, from its creation to its destruction.
5
+ instance's life, from its creation and rendering to its destruction.
6
6
 
7
- This guide will walk you through the entire lifecycle, starting with the initial synchronous steps and moving on to
8
- the asynchronous parts and destruction.
7
+ This guide will walk you through the entire lifecycle, which can be broken down into four main phases:
8
+
9
+ 1. **Synchronous Creation**: The initial, synchronous setup of the instance and its configuration.
10
+ 2. **Asynchronous Initialization**: An optional phase for asynchronous tasks like data fetching.
11
+ 3. **Mounting & Unmounting**: The dynamic phase where a component is rendered into the DOM, and potentially unmounted
12
+ and re-mounted.
13
+ 4. **Destruction**: The final cleanup phase.
9
14
 
10
15
  ## How the Lifecycle is Triggered
11
16
 
@@ -16,73 +21,65 @@ The most common way to create component instances is declaratively, by defining
16
21
  `items` array. The framework then internally uses `Neo.create()` to turn these configuration objects into fully-fledged
17
22
  instances, automatically initiating their lifecycle.
18
23
 
19
- It is crucial to **never** create a Neo.mjs class instance using the `new` keyword (e.g., `new MyComponent()`),
20
- as this would bypass the entire lifecycle initialization process described below, resulting in a broken and
21
- improperly configured instance. Always let the framework handle instantiation, either through declarative `items`
22
- configs or, in less common cases, by using `Neo.create()` directly.
24
+ It is crucial to **never** create a Neo.mjs class instance using the `new` keyword (e.g., `new MyComponent()`), as this
25
+ would bypass the entire lifecycle initialization process described below, resulting in a broken and improperly configured
26
+ instance. Always let the framework handle instantiation.
23
27
 
24
28
  ## 1. The Synchronous Creation Flow
25
29
 
26
30
  When the framework creates a new instance, it executes a sequence of synchronous methods. This initial phase is
27
- responsible for setting up the instance's basic configuration and state.
31
+ responsible for setting up the instance's basic configuration and state. **At this stage, the component has not been
32
+ rendered to the DOM.**
28
33
 
29
34
  The synchronous lifecycle methods are called in the following order:
30
35
 
31
36
  1. **`new YourClass()`**: The framework first calls the actual JavaScript class constructor with **no arguments**.
32
- This is a crucial step. Its primary purpose is to create the instance and initialize all of its defined class
33
- fields. This ensures that by the time any Neo.mjs lifecycle method (like `construct`) or config hook
34
- (like `beforeGetX`) is called, all class fields are fully available on `this`, preventing potential race
35
- conditions or errors from accessing uninitialized properties.
37
+ Its primary purpose is to create the instance and initialize all of its defined class fields. This ensures that by
38
+ the time any Neo.mjs lifecycle method is called, all class fields are fully available on `this`.
36
39
 
37
- 2. **`construct(config)`**: This is the first Neo.mjs lifecycle hook called on the new instance. Its primary role is
38
- to process the configuration object that was passed to `Neo.create()`. It's here that the initial values for
39
- your configs are processed and applied via the config system.
40
+ 2. **`construct(config)`**: This is the first Neo.mjs lifecycle hook. Its primary role is to process the configuration
41
+ object passed to `Neo.create()`. It's here that the initial values for your configs are processed and applied via
42
+ the config system.
40
43
 
41
44
  3. **`onConstructed()`**: This hook is called immediately after `construct()` has finished. It's the ideal place to
42
- perform any setup that depends on the initial configuration, such as setting initial values for other
43
- properties or starting a process.
45
+ perform any setup that depends on the initial configuration. **Crucially, do not attempt to access the DOM here**,
46
+ as the component is not yet rendered.
44
47
 
45
- 4. **`onAfterConstructed()`**: This hook is called after `onConstructed()`. It provides another opportunity for
46
- setup logic, which can be useful for separating concerns or for logic that needs to run after the primary
47
- `onConstructed` logic has completed.
48
+ 4. **`onAfterConstructed()`**: This hook is called after `onConstructed()`. It provides another opportunity for setup logic.
48
49
 
49
50
  5. **`init()`**: This is the final synchronous hook in the creation process. It's a general-purpose initialization
50
- method that you can use for any final setup tasks before the instance is returned by `Neo.create()`.
51
-
52
- It's important to remember that all of these methods are synchronous. Any asynchronous operations should be handled
53
- in the later, asynchronous phases of the lifecycle.
51
+ method for any final setup tasks.
54
52
 
55
- ## 2. `constructor()` vs `construct()`: A Critical Distinction
53
+ ### `constructor()` vs `construct()`: A Critical Distinction
56
54
 
57
- While you *can* define a standard JavaScript `constructor()` method on a Neo.mjs class, it is strongly discouraged
58
- and considered a bad practice. The framework provides the `construct()` lifecycle hook for a very specific and
59
- powerful reason: **pre-processing configs**.
55
+ While you *can* define a standard JavaScript `constructor()` method on a Neo.mjs class, it is strongly discouraged and
56
+ considered a bad practice. The framework provides the `construct()` lifecycle hook for a very specific and powerful
57
+ reason: **pre-processing configs**.
60
58
 
61
- ### The `constructor()` Limitation
59
+ #### The `constructor()` Limitation
62
60
 
63
61
  In standard JavaScript class inheritance, you **cannot** access the `this` context in a constructor before calling
64
62
  `super()`. This is a language-level restriction.
65
63
 
66
- ```javascript
64
+ ```javascript readonly
67
65
  // Anti-pattern: Do not do this in Neo.mjs
68
66
  constructor(config) {
69
67
  // ERROR! 'this' is not available before super()
70
- console.log(this.someClassField);
68
+ console.log(this.someClassField);
71
69
 
72
70
  super(config); // Assuming a parent constructor call
73
71
  }
74
72
  ```
75
73
 
76
- ### The `construct()` Advantage
74
+ #### The `construct()` Advantage
77
75
 
78
- The `construct()` method, however, is just a regular method called by the framework *after* the instance has been
79
- fully created (via `new YourClass()`). This means that inside `construct()`, you have full access to `this` from the
80
- very first line.
76
+ The `construct()` method, however, is just a regular method called by the framework *after* the instance has been fully
77
+ created (via `new YourClass()`). This means that inside `construct()`, you have full access to `this` from the very first line.
81
78
 
82
79
  This enables a powerful pattern: you can inspect or modify the incoming `config` object *before* passing it up the
83
80
  inheritance chain with `super.construct(config)`. This is invaluable for component-specific logic.
84
81
 
85
- ```javascript
82
+ ```javascript readonly
86
83
  // The correct Neo.mjs pattern
87
84
  construct(config) {
88
85
  // 'this' is fully available here!
@@ -97,53 +94,52 @@ construct(config) {
97
94
  }
98
95
  ```
99
96
 
100
- In summary, always use `construct()` for your initialization logic. It provides the flexibility needed to work
101
- within the Neo.mjs lifecycle and config system, a flexibility that the standard `constructor()` cannot offer.
102
-
103
- ## 3. The Asynchronous Initialization Flow
97
+ In summary, always use `construct()` for your initialization logic. It provides the flexibility needed to work within
98
+ the Neo.mjs lifecycle and config system, a flexibility that the standard `constructor()` cannot offer.
104
99
 
105
- After the synchronous creation methods are complete, the instance lifecycle moves into an asynchronous phase. This is
106
- where you should place any logic that cannot be executed synchronously, such as loading external files, fetching
107
- data from a server, or waiting for other resources to become available.
100
+ ## 2. The Asynchronous Initialization Flow
108
101
 
109
- This phase is orchestrated by a microtask scheduled from within the `construct()` method.
102
+ After the synchronous creation methods are complete, the instance lifecycle moves into an optional asynchronous phase.
103
+ This is where you should place any logic that cannot be executed synchronously, such as loading external files or fetching data.
110
104
 
111
105
  ### `initAsync()`: The Asynchronous Entry Point
112
106
 
113
- The core of this phase is the `async initAsync()` method.
107
+ The core of this phase is the `async initAsync()` method. It is scheduled as a microtask from within `construct()`.
114
108
 
115
- * **Scheduling**: Immediately after the synchronous `construct()` logic is finished, the framework schedules a
116
- microtask (`Promise.resolve().then(...)`) that will execute after the current JavaScript execution block is empty.
117
- * **Execution**: This microtask calls and `await`s the `initAsync()` method. This is the designated place for all
118
- asynchronous initialization logic. You can override this method in your own classes to perform tasks like
119
- dynamic imports or initial data fetching.
120
- * **Parent Call**: When overriding `initAsync()`, it is crucial to call `await super.initAsync()` at the beginning
121
- of your implementation to ensure that parent classes can perform their own asynchronous setup, such as
122
- registering remote methods.
109
+ * **Execution**: This microtask calls and `await`s `initAsync()`. This is the designated place for all asynchronous
110
+ initialization logic. When overriding `initAsync()`, it is crucial to call `await super.initAsync()` at the
111
+ beginning of your implementation.
123
112
 
124
- ```javascript
113
+ ```javascript readonly
125
114
  // In your class
126
115
  async initAsync() {
127
116
  // Always call the parent method first!
128
117
  await super.initAsync();
129
118
 
130
- // Your async logic here
131
- const myModule = await import('./MyOptionalModule.mjs');
132
- this.data = await myService.fetchInitialData();
119
+ // Your async logic here, wrapped in a try...catch block
120
+ try {
121
+ const myModule = await import('./MyOptionalModule.mjs');
122
+ this.data = await myService.fetchInitialData();
123
+ } catch (e) {
124
+ console.error('Failed to initialize component asynchronously', e);
125
+ // Handle the error appropriately, e.g., by setting an error state on the component.
126
+ }
133
127
  }
134
128
  ```
135
129
 
130
+ **Important**: If the `initAsync()` method throws an error, the `isReady` flag will never be set to `true`. It is crucial
131
+ to wrap your asynchronous logic in `try...catch` blocks to handle potential failures gracefully.
132
+
136
133
  ### `isReady`: The Signal of Completion
137
134
 
138
- Once the `initAsync()` promise resolves, the framework sets the instance's `isReady` config to `true`.
135
+ Once the `initAsync()` promise resolves successfully, the framework sets the instance's `isReady` config to `true`.
139
136
 
140
- * **`isReady_`**: The config is defined as `isReady_` (with a trailing underscore), which means it gets an
141
- `afterSetIsReady(value, oldValue)` hook.
142
- * **Reacting to Readiness**: You can implement the `afterSetIsReady()` method to be notified precisely when the
143
- instance is fully initialized and ready for interaction. This is the most reliable way to coordinate logic that
144
- depends on the component's full readiness.
137
+ * **`isReady_`**: The config is defined as `isReady_`, which means it gets an `afterSetIsReady(value, oldValue)` hook.
138
+ * **Reacting to Readiness**: You can implement `afterSetIsReady()` to be notified precisely when the instance is fully
139
+ * initialized and ready for interaction. This is the most reliable way to coordinate logic that depends on the
140
+ * component's full readiness.
145
141
 
146
- ```javascript
142
+ ```javascript readonly
147
143
  // In your class
148
144
  afterSetIsReady(isReady, wasReady) {
149
145
  if (isReady && !wasReady) {
@@ -153,8 +149,50 @@ afterSetIsReady(isReady, wasReady) {
153
149
  }
154
150
  ```
155
151
 
156
- This `initAsync` -> `isReady` pattern provides a robust and predictable way to manage the asynchronous parts of the
157
- instance lifecycle, ensuring that dependent logic only runs when the instance is in a known, ready state.
152
+ ## 3. The Mounting Phase: Interaction with the DOM
153
+
154
+ This phase is what truly sets Neo.mjs apart. A component's lifecycle is not complete after initialization; it only
155
+ becomes fully interactive once it is **mounted** into the DOM. Furthermore, a component can be unmounted and re-mounted
156
+ multiple times, even into different browser windows, all while preserving its instance and state.
157
+
158
+ ### `mounted`: The DOM-Ready Signal
159
+
160
+ The `mounted_` config is the key to this phase. It is a boolean flag that indicates whether the component is currently
161
+ rendered in the DOM.
162
+
163
+ * **`afterSetMounted(isMounted, wasMounted)`**: This is the most important hook for DOM interaction. It is called with
164
+ * `true` when the component's VDOM is successfully rendered into the DOM, and with `false` when it is removed.
165
+
166
+ **This is the only safe and reliable place to perform DOM measurements or manipulations.**
167
+
168
+ ```javascript readonly
169
+ // In your component class
170
+ afterSetMounted(isMounted, wasMounted) {
171
+ if (isMounted) {
172
+ console.log('Component is now in the DOM!');
173
+ // It is now safe to measure this.vnode.dom
174
+ const rect = this.vnode.dom.getBoundingClientRect();
175
+ console.log('Component dimensions:', rect.width, rect.height);
176
+ } else {
177
+ console.log('Component was removed from the DOM.');
178
+ }
179
+ }
180
+ ```
181
+
182
+ ### The Re-Mounting Lifecycle
183
+
184
+ Because a component can be removed from a container and added back later, the `afterSetMounted` hook can fire multiple
185
+ times. This allows you to correctly manage DOM-related resources, such as third-party libraries or complex event
186
+ listeners, that need to be created and destroyed in sync with the component's presence in the DOM.
187
+
188
+ ### Multi-Window Mounting
189
+
190
+ Neo.mjs's multi-window support adds another layer to this phase. A component can be unmounted from one browser window
191
+ and re-mounted into another. The framework manages this through the `windowId_` config.
192
+
193
+ * **`windowId_`**: This config tracks which browser window the component currently belongs to.
194
+ * **`afterSetWindowId(newWindowId, oldWindowId)`**: This hook is called when a component is moved between windows.
195
+ * You can use it to manage any window-specific resources or logic.
158
196
 
159
197
  ## 4. Destruction: Cleaning Up with `destroy()`
160
198
 
@@ -182,7 +220,7 @@ registrations so that the instance can be safely garbage collected.
182
220
 
183
221
  Here is an example from `Neo.grid.Container` that illustrates key best practices:
184
222
 
185
- ```javascript
223
+ ```javascript readonly
186
224
  // Example from src/grid/Container.mjs
187
225
  destroy(...args) {
188
226
  let me = this;
@@ -219,25 +257,24 @@ To summarize the best practices:
219
257
  2. **Destroy Owned Instances**: If your class creates its own instances of other Neo.mjs classes (e.g., helpers,
220
258
  managers), you are responsible for calling `destroy()` on them.
221
259
  3. **Clean Up Shared Instances**: If your class uses a shared instance (like a `Store` or a global service), do **not**
222
- call `destroy()` on it. Instead, remove any listeners you added to it. A good pattern is to set the config
223
- property to `null` (e.g., `this.store = null`) and perform the listener cleanup inside the `afterSet` hook.
260
+ call `destroy()` on it. Instead, remove any listeners you added to it. A good pattern is to set the config property
261
+ to `null` (e.g., `this.store = null`) and perform the listener cleanup inside the `afterSet` hook.
224
262
  4. **Unregister from Services**: If your class registered itself with any external manager or service (like the
225
263
  `ResizeObserver`), be sure to unregister from it.
226
264
 
227
265
  ## 5. Lifecycle of Nested Instances: Set-Driven vs. Get-Driven
228
266
 
229
267
  A powerful feature of the config system is that a config property can be another Neo.mjs class instance. A common
230
- example is a grid's `selectionModel`. This raises an important architectural question: when should this nested
231
- instance be created? The framework supports two patterns, each with different implications for the lifecycle.
268
+ example is a grid's `selectionModel`. This raises an important architectural question: when should this nested instance
269
+ be created? The framework supports two patterns, each with different implications for the lifecycle.
232
270
 
233
271
  ### The Set-Driven Approach (Eager Instantiation)
234
272
 
235
- In this pattern, you ensure the instance is created as soon as the config is set. This is typically done inside a
236
- `beforeSet` hook.
273
+ In this pattern, you ensure the instance is created as soon as the config is set. This is typically done inside a `beforeSet` hook.
237
274
 
238
275
  The `Neo.grid.Body` class provides a perfect example with its `selectionModel_` config.
239
276
 
240
- ```javascript
277
+ ```javascript readonly
241
278
  // In Neo.grid.Body
242
279
  beforeSetSelectionModel(value, oldValue) {
243
280
  oldValue?.destroy();
@@ -248,13 +285,13 @@ beforeSetSelectionModel(value, oldValue) {
248
285
  }
249
286
  ```
250
287
 
251
- When the framework processes the grid body's configs during its `construct` phase, `beforeSetSelectionModel` is
252
- called. It immediately creates the selection model instance.
288
+ When the framework processes the grid body's configs during its `construct` phase, `beforeSetSelectionModel` is called.
289
+ It immediately creates the selection model instance.
253
290
 
254
- **The key takeaway is the guarantee this provides for `onConstructed()`**. Because the selection model was
255
- instantiated during `construct`, by the time `onConstructed()` is called, you can safely assume the instance exists.
291
+ **The key takeaway is the guarantee this provides for `onConstructed()`**. Because the selection model was instantiated
292
+ during `construct`, by the time `onConstructed()` is called, you can safely assume the instance exists.
256
293
 
257
- ```javascript
294
+ ```javascript readonly
258
295
  // In Neo.grid.Body
259
296
  onConstructed() {
260
297
  super.onConstructed();
@@ -264,18 +301,18 @@ onConstructed() {
264
301
  }
265
302
  ```
266
303
 
267
- Use the set-driven approach when a nested instance is **essential** for the component's core functionality and needs
268
- to be available immediately after construction.
304
+ Use the set-driven approach when a nested instance is **essential** for the component's core functionality and needs to
305
+ be available immediately after construction.
269
306
 
270
307
  ### The Get-Driven Approach (Lazy Instantiation)
271
308
 
272
- Alternatively, you can defer the creation of a nested instance until it's actually needed for the first time. This
273
- is achieved by creating the instance within a `beforeGet` hook. This "lazy" approach can improve initial creation
309
+ Alternatively, you can defer the creation of a nested instance until it's actually needed for the first time. This is
310
+ achieved by creating the instance within a `beforeGet` hook. This "lazy" approach can improve initial creation
274
311
  performance if the nested instance is complex or not always used.
275
312
 
276
313
  `Neo.grid.Body` also demonstrates this pattern with its `columnPositions_` config.
277
314
 
278
- ```javascript
315
+ ```javascript readonly
279
316
  // In Neo.grid.Body
280
317
  beforeGetColumnPositions(value) {
281
318
  // If the backing field (_columnPositions) is null...
package/learn/tree.json CHANGED
@@ -64,6 +64,7 @@
64
64
  {"name": "Rock Scissors Paper", "parentId": "Tutorials", "id": "tutorials/RSP", "hidden": true},
65
65
  {"name": "Earthquakes", "parentId": "Tutorials", "id": "tutorials/Earthquakes"},
66
66
  {"name": "Todo List", "parentId": "Tutorials", "id": "tutorials/TodoList"},
67
+ {"name": "Creating a Functional Button", "parentId": "Tutorials", "id": "tutorials/CreatingAFunctionalButton"},
67
68
  {"name": "JavaScript Classes", "parentId": null, "isLeaf": false, "id": "JavaScript", "collapsed": true},
68
69
  {"name": "Classes, Properties, and Methods", "parentId": "JavaScript", "id": "javascript/Classes"},
69
70
  {"name": "Overriding Methods", "parentId": "JavaScript", "id": "javascript/Overrides"},
@@ -0,0 +1,179 @@
1
+ # Creating a Custom Functional Button
2
+
3
+ In the "Describing a View" guide, you learned the basics of functional and class-based components. Now, let's dive
4
+ deeper into the modern approach by creating our own custom, reusable functional button.
5
+
6
+ **Note:** Neo.mjs already provides a powerful, feature-rich functional button (`Neo.functional.button.Base`). The purpose
7
+ of this guide is not to replace it, but to use a button as a simple, practical example to teach you the fundamentals of
8
+ creating your own functional components.
9
+
10
+ This guide will walk you through the process of building a `MyCoolButton` component that has its own unique style and
11
+ behavior, using the `defineComponent` helper.
12
+
13
+ ## 1. Defining the Component
14
+
15
+ First, let's create the basic structure of our component. We'll use `defineComponent` and provide a `className`
16
+ and a `createVdom` method.
17
+
18
+ ```javascript readonly
19
+ import {defineComponent} from '../../src/functional/_export.mjs';
20
+
21
+ const MyCoolButton = defineComponent({
22
+ className: 'My.CoolButton',
23
+
24
+ createVdom(config) {
25
+ // We will build our VDOM here
26
+ return {
27
+ tag : 'button',
28
+ cls : ['my-cool-button'],
29
+ text: 'Click Me'
30
+ }
31
+ }
32
+ });
33
+
34
+ export default MyCoolButton;
35
+ ```
36
+
37
+ ## 2. Adding Custom Configs
38
+
39
+ A component isn't very reusable without configs. Let's add `text_` and `iconCls_` to our component's public API.
40
+ Remember, the trailing underscore `_` makes the config reactive.
41
+
42
+ We'll also update `createVdom` to use these configs. The `config` parameter of `createVdom` is a reactive proxy to the
43
+ component's instance, so we can access our configs directly from it.
44
+
45
+ ```javascript readonly
46
+ import {defineComponent} from '../../src/functional/_export.mjs';
47
+
48
+ const MyCoolButton = defineComponent({
49
+ className: 'My.CoolButton',
50
+
51
+ // 1. Define the public API
52
+ config: {
53
+ iconCls_: null,
54
+ text_ : 'Default Text'
55
+ },
56
+
57
+ // 2. Use the configs in createVdom
58
+ createVdom(config) {
59
+ const {iconCls, text} = config;
60
+
61
+ return {
62
+ tag: 'button',
63
+ cls: ['my-cool-button'],
64
+ cn : [{
65
+ tag : 'span',
66
+ cls : ['fa', iconCls],
67
+ removeDom: !iconCls // Don't render the span if no iconCls is provided
68
+ }, {
69
+ tag: 'span',
70
+ cls: ['my-cool-button-text'],
71
+ text
72
+ }]
73
+ }
74
+ }
75
+ });
76
+
77
+ export default MyCoolButton;
78
+ ```
79
+
80
+ ## 3. Handling User Events
81
+
82
+ Static buttons are boring. Let's make it interactive. We can add a `handler_` config and an `onClick` method to our
83
+ component. The `addDomListeners` method in the `construct` hook allows us to listen for native DOM events.
84
+
85
+ ```javascript readonly
86
+ import {defineComponent} from '../../src/functional/_export.mjs';
87
+
88
+ const MyCoolButton = defineComponent({
89
+ className: 'My.CoolButton',
90
+
91
+ config: {
92
+ handler_: null, // A function to call on click
93
+ iconCls_: null,
94
+ text_ : 'Default Text'
95
+ },
96
+
97
+ construct(config) {
98
+ // The super.construct call is important!
99
+ // It sets up the component's lifecycle and effects.
100
+ this.super(config);
101
+
102
+ this.addDomListeners({
103
+ click: this.onClick,
104
+ scope: this
105
+ });
106
+ },
107
+
108
+ createVdom(config) {
109
+ // ... (same as before)
110
+ },
111
+
112
+ onClick(data) {
113
+ // If a handler function is provided, call it.
114
+ this.handler?.(this);
115
+ }
116
+ });
117
+
118
+ export default MyCoolButton;
119
+ ```
120
+
121
+ ## 4. Using Your Custom Component
122
+
123
+ Now you can use `MyCoolButton` just like any other Neo.mjs component, either in a functional or a class-based view.
124
+
125
+ ```javascript live-preview
126
+ import {defineComponent} from '../functional/_export.mjs';
127
+ import Container from '../container/Base.mjs';
128
+
129
+ // 1. Define our custom button
130
+ const MyCoolButton = defineComponent({
131
+ className: 'My.CoolButton',
132
+ config: {
133
+ handler_: null,
134
+ iconCls_: null,
135
+ text_ : 'Default Text'
136
+ },
137
+ construct(config) {
138
+ this.super(config);
139
+ this.addDomListeners({click: this.onClick, scope: this});
140
+ },
141
+ createVdom(config) {
142
+ const {iconCls, text} = config;
143
+ return {
144
+ tag: 'button',
145
+ cls: ['my-cool-button'],
146
+ cn : [
147
+ {tag: 'span', cls: ['fa', iconCls], removeDom: !iconCls},
148
+ {tag: 'span', cls: ['my-cool-button-text'], text}
149
+ ]
150
+ }
151
+ },
152
+ onClick(data) {
153
+ this.handler?.(this);
154
+ }
155
+ });
156
+
157
+ // 2. Use it in a MainView
158
+ class MainView extends Container {
159
+ static config = {
160
+ className: 'GS.guides.CustomButtonMainView',
161
+ layout : {ntype: 'vbox', align: 'start'},
162
+ items : [{
163
+ module : MyCoolButton,
164
+ iconCls: 'fa-star',
165
+ text : 'My Button!',
166
+ handler(button) {
167
+ console.log('Button clicked!', button);
168
+ button.text = 'Clicked!'; // It's reactive!
169
+ }
170
+ }]
171
+ }
172
+ }
173
+
174
+ MainView = Neo.setupClass(MainView);
175
+ ```
176
+
177
+ This example demonstrates the full power of the functional component model. You can quickly create reusable, reactive,
178
+ and encapsulated components with a clean and modern API. From here, you could add more configs, more complex VDOM logic,
179
+ or even internal state using the `useConfig` hook to build even more powerful components.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name" : "neo.mjs",
3
- "version" : "10.1.1",
3
+ "version" : "10.2.1",
4
4
  "description" : "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type" : "module",
6
6
  "repository" : {
@@ -88,7 +88,7 @@
88
88
  "fs-extra" : "^11.3.0",
89
89
  "highlightjs-line-numbers.js" : "^2.9.0",
90
90
  "html-minifier-terser" : "^7.2.0",
91
- "inquirer" : "^12.8.2",
91
+ "inquirer" : "^12.9.0",
92
92
  "marked" : "^16.1.1",
93
93
  "monaco-editor" : "0.50.0",
94
94
  "neo-jsdoc" : "1.0.1",
@@ -98,7 +98,7 @@
98
98
  "siesta-lite" : "5.5.2",
99
99
  "terser" : "^5.43.1",
100
100
  "url" : "^0.11.4",
101
- "webpack" : "^5.100.2",
101
+ "webpack" : "^5.101.0",
102
102
  "webpack-cli" : "^6.0.1",
103
103
  "webpack-dev-server" : "^5.2.2",
104
104
  "webpack-hook-plugin" : "^1.0.7",
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.1.1'
302
+ * @default '10.2.1'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.1.1'
307
+ version: '10.2.1'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -109,7 +109,10 @@ class Button extends Component {
109
109
  route_: null,
110
110
  /**
111
111
  * The text displayed on the button [optional]
112
- * @member {String|null} text=null
112
+ * You can either pass a string, or a vdom cn array.
113
+ * @example
114
+ * text: [{tag: 'span', style: {color: '#bbbbbb'}, text: '●'}, {vtype: 'text', text: ' Cases'}]
115
+ * @member {Object[]|String|null} text=null
113
116
  * @reactive
114
117
  */
115
118
  text: null,
@@ -378,8 +381,8 @@ class Button extends Component {
378
381
 
379
382
  /**
380
383
  * Triggered after the text config got changed
381
- * @param {String|null} value
382
- * @param {String|null} oldValue
384
+ * @param {Object[]|String|null} value
385
+ * @param {Object[]|String|null} oldValue
383
386
  * @protected
384
387
  */
385
388
  afterSetText(value, oldValue) {
@@ -393,7 +396,13 @@ class Button extends Component {
393
396
  textNode.removeDom = isEmpty;
394
397
 
395
398
  if (!isEmpty) {
396
- textNode.text = value
399
+ if (Neo.isArray(value)) {
400
+ textNode.cn = value;
401
+ delete textNode.text
402
+ } else {
403
+ textNode.text = value;
404
+ delete textNode.cn
405
+ }
397
406
  }
398
407
 
399
408
  me.update()
@@ -671,10 +671,17 @@ class Container extends Component {
671
671
  *
672
672
  */
673
673
  onConstructed() {
674
- let me = this;
674
+ let me = this,
675
+ layoutConfig = me.layout;
676
+
677
+ // If the layout is a config object (not an instance), deep clone it
678
+ // to prevent prototype pollution.
679
+ if (layoutConfig && !(layoutConfig instanceof LayoutBase)) {
680
+ layoutConfig = Neo.clone(layoutConfig, true)
681
+ }
675
682
 
676
683
  // in case the Container does not have a layout config, the setter won't trigger
677
- me._layout = me.createLayout(me.layout);
684
+ me._layout = me.createLayout(layoutConfig);
678
685
  me._layout?.applyRenderAttributes();
679
686
 
680
687
  super.onConstructed();
@@ -170,7 +170,9 @@ class Store extends Base {
170
170
 
171
171
  me.isLoading = false;
172
172
 
173
- me.add(value)
173
+ me.add(value);
174
+
175
+ me.isLoaded = true
174
176
  }
175
177
  }
176
178
  }
@@ -386,6 +388,7 @@ class Store extends Base {
386
388
  if (response.success) {
387
389
  me.totalCount = response.totalCount;
388
390
  me.data = Neo.ns(me.responseRoot, false, response); // fires the load event
391
+ me.isLoaded = true;
389
392
 
390
393
  return me.data
391
394
  }
@@ -402,6 +405,8 @@ class Store extends Base {
402
405
  me.data = Neo.ns(me.responseRoot, false, data.json) || data.json // fires the load event
403
406
  }
404
407
 
408
+ me.isLoaded = true;
409
+
405
410
  return data?.json || null
406
411
  } catch(err) {
407
412
  console.error('Error for Neo.Xhr.request', {id: me.id, error: err, url: opts.url});
@@ -446,8 +451,8 @@ class Store extends Base {
446
451
 
447
452
  // Being constructed does not mean that related afterSetStore() methods got executed
448
453
  // => break the sync flow to ensure potential listeners got applied
449
- me.timeout(1).then(() => {
450
- if (me.getCount() > 0) {
454
+ Promise.resolve().then(() => {
455
+ if (me.isLoaded) {
451
456
  me.fire('load', me.items)
452
457
  } else if (me.autoLoad) {
453
458
  me.load()