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.
- package/.github/RELEASE_NOTES/v10.2.0.md +34 -0
- package/.github/RELEASE_NOTES/v10.2.1.md +17 -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/WorldMapContainer.mjs +4 -4
- package/apps/covid/view/country/Gallery.mjs +1 -1
- package/apps/covid/view/country/Helix.mjs +1 -1
- package/apps/covid/view/country/Table.mjs +27 -29
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +12 -0
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/sharedcovid/view/GalleryContainer.mjs +1 -1
- package/apps/sharedcovid/view/HelixContainer.mjs +1 -1
- package/apps/sharedcovid/view/WorldMapContainer.mjs +4 -4
- package/apps/sharedcovid/view/country/Gallery.mjs +1 -1
- package/apps/sharedcovid/view/country/Helix.mjs +1 -1
- package/apps/sharedcovid/view/country/Table.mjs +22 -22
- package/examples/grid/bigData/ControlsContainer.mjs +14 -0
- package/examples/stateProvider/inline/MainContainer.mjs +1 -1
- package/examples/stateProvider/twoWay/MainContainer.mjs +2 -2
- package/examples/treeAccordion/MainContainer.mjs +1 -1
- package/learn/blog/v10-deep-dive-functional-components.md +107 -97
- package/learn/blog/v10-deep-dive-reactivity.md +3 -3
- package/learn/blog/v10-deep-dive-state-provider.md +42 -137
- package/learn/blog/v10-deep-dive-vdom-revolution.md +35 -61
- package/learn/blog/v10-post1-love-story.md +3 -3
- package/learn/gettingstarted/DescribingTheUI.md +108 -33
- package/learn/guides/fundamentals/ConfigSystemDeepDive.md +118 -18
- package/learn/guides/fundamentals/InstanceLifecycle.md +121 -84
- package/learn/tree.json +1 -0
- package/learn/tutorials/CreatingAFunctionalButton.md +179 -0
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/button/Base.mjs +13 -4
- package/src/container/Base.mjs +9 -2
- package/src/data/Store.mjs +8 -3
- package/src/date/SelectorContainer.mjs +2 -2
- package/src/form/field/Base.mjs +15 -1
- package/src/form/field/ComboBox.mjs +5 -15
- package/src/functional/component/Base.mjs +26 -0
- package/src/functional/util/html.mjs +75 -0
- package/src/state/Provider.mjs +7 -4
- package/src/tree/Accordion.mjs +1 -1
- package/test/siesta/siesta.js +8 -1
- package/test/siesta/tests/form/field/AfterSetValueSequence.mjs +106 -0
- package/test/siesta/tests/functional/HtmlTemplateComponent.mjs +92 -0
- package/test/siesta/tests/state/FeedbackLoop.mjs +159 -0
- 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,
|
8
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
33
|
-
|
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
|
38
|
-
|
39
|
-
|
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,
|
43
|
-
|
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
|
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
|
-
|
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
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
* **
|
116
|
-
|
117
|
-
|
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
|
-
|
132
|
-
|
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_
|
141
|
-
|
142
|
-
*
|
143
|
-
|
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
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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",
|
package/src/DefaultConfig.mjs
CHANGED
@@ -299,12 +299,12 @@ const DefaultConfig = {
|
|
299
299
|
useVdomWorker: true,
|
300
300
|
/**
|
301
301
|
* buildScripts/injectPackageVersion.mjs will update this value
|
302
|
-
* @default '10.
|
302
|
+
* @default '10.2.1'
|
303
303
|
* @memberOf! module:Neo
|
304
304
|
* @name config.version
|
305
305
|
* @type String
|
306
306
|
*/
|
307
|
-
version: '10.
|
307
|
+
version: '10.2.1'
|
308
308
|
};
|
309
309
|
|
310
310
|
Object.assign(DefaultConfig, {
|
package/src/button/Base.mjs
CHANGED
@@ -109,7 +109,10 @@ class Button extends Component {
|
|
109
109
|
route_: null,
|
110
110
|
/**
|
111
111
|
* The text displayed on the button [optional]
|
112
|
-
*
|
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
|
-
|
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()
|
package/src/container/Base.mjs
CHANGED
@@ -671,10 +671,17 @@ class Container extends Component {
|
|
671
671
|
*
|
672
672
|
*/
|
673
673
|
onConstructed() {
|
674
|
-
let me
|
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(
|
684
|
+
me._layout = me.createLayout(layoutConfig);
|
678
685
|
me._layout?.applyRenderAttributes();
|
679
686
|
|
680
687
|
super.onConstructed();
|
package/src/data/Store.mjs
CHANGED
@@ -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
|
-
|
450
|
-
if (me.
|
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()
|