neo.mjs 10.0.0-beta.1 → 10.0.0-beta.3
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/ServiceWorker.mjs +2 -2
- package/apps/colors/view/GridContainer.mjs +1 -1
- package/apps/covid/view/AttributionComponent.mjs +1 -1
- package/apps/covid/view/HeaderContainer.mjs +6 -6
- package/apps/covid/view/MainContainerController.mjs +5 -5
- package/apps/covid/view/TableContainerController.mjs +1 -1
- package/apps/covid/view/country/Gallery.mjs +13 -13
- package/apps/covid/view/country/Helix.mjs +13 -13
- package/apps/covid/view/country/HistoricalDataTable.mjs +1 -1
- package/apps/email/view/Viewport.mjs +2 -2
- package/apps/form/view/FormPageContainer.mjs +2 -3
- package/apps/form/view/SideNavList.mjs +1 -1
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/examples_dist_esm.json +1 -1
- package/apps/portal/resources/data/examples_dist_prod.json +2 -2
- package/apps/portal/view/HeaderToolbar.mjs +3 -3
- package/apps/portal/view/about/Container.mjs +2 -2
- package/apps/portal/view/about/MemberContainer.mjs +3 -3
- package/apps/portal/view/blog/List.mjs +7 -7
- package/apps/portal/view/examples/List.mjs +4 -4
- package/apps/portal/view/home/ContentBox.mjs +2 -2
- package/apps/portal/view/home/FeatureSection.mjs +3 -3
- package/apps/portal/view/home/FooterContainer.mjs +7 -7
- package/apps/portal/view/home/parts/AfterMath.mjs +3 -3
- package/apps/portal/view/home/parts/MainNeo.mjs +3 -3
- package/apps/portal/view/home/parts/References.mjs +6 -6
- package/apps/portal/view/learn/ContentComponent.mjs +18 -11
- package/apps/portal/view/learn/PageSectionsContainer.mjs +1 -1
- package/apps/portal/view/learn/PageSectionsList.mjs +2 -2
- package/apps/portal/view/services/Component.mjs +16 -16
- package/apps/realworld/view/FooterComponent.mjs +1 -1
- package/apps/realworld/view/HeaderComponent.mjs +8 -8
- package/apps/realworld/view/HomeComponent.mjs +6 -6
- package/apps/realworld/view/article/CommentComponent.mjs +4 -4
- package/apps/realworld/view/article/Component.mjs +14 -14
- package/apps/realworld/view/article/CreateCommentComponent.mjs +3 -3
- package/apps/realworld/view/article/CreateComponent.mjs +3 -3
- package/apps/realworld/view/article/PreviewComponent.mjs +1 -1
- package/apps/realworld/view/article/TagListComponent.mjs +2 -2
- package/apps/realworld/view/user/ProfileComponent.mjs +8 -8
- package/apps/realworld/view/user/SettingsComponent.mjs +4 -4
- package/apps/realworld/view/user/SignUpComponent.mjs +4 -4
- package/apps/realworld2/view/FooterComponent.mjs +1 -1
- package/apps/realworld2/view/HomeContainer.mjs +3 -3
- package/apps/realworld2/view/article/DetailsContainer.mjs +1 -1
- package/apps/realworld2/view/article/PreviewComponent.mjs +7 -7
- package/apps/realworld2/view/article/TagListComponent.mjs +2 -2
- package/apps/realworld2/view/user/ProfileContainer.mjs +1 -1
- package/apps/route/view/center/CardAdministration.mjs +2 -2
- package/apps/route/view/center/CardAdministrationDenied.mjs +1 -1
- package/apps/route/view/center/CardContact.mjs +2 -2
- package/apps/route/view/center/CardHome.mjs +1 -1
- package/apps/route/view/center/CardSection1.mjs +1 -1
- package/apps/route/view/center/CardSection2.mjs +1 -1
- package/apps/sharedcovid/view/AttributionComponent.mjs +1 -1
- package/apps/sharedcovid/view/HeaderContainer.mjs +6 -6
- package/apps/sharedcovid/view/MainContainerController.mjs +5 -5
- package/apps/sharedcovid/view/TableContainerController.mjs +1 -1
- package/apps/sharedcovid/view/country/Gallery.mjs +13 -13
- package/apps/sharedcovid/view/country/Helix.mjs +13 -13
- package/apps/sharedcovid/view/country/HistoricalDataTable.mjs +1 -1
- package/apps/shareddialog/childapps/shareddialog2/view/MainContainer.mjs +1 -1
- package/apps/shareddialog/view/MainContainer.mjs +1 -1
- package/buildScripts/createApp.mjs +2 -2
- package/learn/Glossary.md +261 -0
- package/learn/README.md +9 -14
- package/learn/benefits/ConfigSystem.md +536 -26
- package/learn/benefits/Effort.md +47 -2
- package/learn/benefits/Features.md +50 -32
- package/learn/benefits/FormsEngine.md +54 -24
- package/learn/benefits/MultiWindow.md +31 -5
- package/learn/benefits/Quick.md +45 -12
- package/learn/benefits/RPCLayer.md +75 -0
- package/learn/benefits/Speed.md +17 -12
- package/learn/guides/Collections.md +436 -0
- package/learn/guides/ConfigSystemDeepDive.md +280 -0
- package/learn/guides/CustomComponents.md +256 -14
- package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +17 -17
- package/learn/guides/ExtendingNeoClasses.md +331 -0
- package/learn/guides/Forms.md +449 -1
- package/learn/guides/InstanceLifecycle.md +295 -1
- package/learn/guides/Layouts.md +246 -1
- package/learn/guides/MainThreadAddons.md +475 -0
- package/learn/guides/Records.md +286 -0
- package/learn/guides/WorkingWithVDom.md +14 -14
- package/learn/guides/form_fields/ComboBox.md +241 -0
- package/learn/tree.json +57 -51
- package/package.json +2 -2
- package/resources/scss/src/apps/portal/learn/ContentComponent.scss +9 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/Main.mjs +8 -7
- package/src/Neo.mjs +16 -2
- package/src/button/Base.mjs +2 -2
- package/src/calendar/view/SettingsContainer.mjs +2 -2
- package/src/calendar/view/YearComponent.mjs +9 -9
- package/src/calendar/view/calendars/ColorsList.mjs +1 -1
- package/src/calendar/view/calendars/List.mjs +1 -1
- package/src/calendar/view/month/Component.mjs +15 -15
- package/src/calendar/view/week/Component.mjs +12 -12
- package/src/calendar/view/week/EventDragZone.mjs +4 -4
- package/src/calendar/view/week/TimeAxisComponent.mjs +3 -3
- package/src/component/Base.mjs +17 -2
- package/src/component/Carousel.mjs +2 -2
- package/src/component/Chip.mjs +3 -3
- package/src/component/Circle.mjs +2 -2
- package/src/component/DateSelector.mjs +8 -8
- package/src/component/Helix.mjs +1 -1
- package/src/component/Label.mjs +3 -18
- package/src/component/Legend.mjs +3 -3
- package/src/component/MagicMoveText.mjs +6 -14
- package/src/component/Process.mjs +3 -3
- package/src/component/Progress.mjs +1 -1
- package/src/component/StatusBadge.mjs +2 -2
- package/src/component/Timer.mjs +2 -2
- package/src/component/Toast.mjs +5 -3
- package/src/container/AccordionItem.mjs +2 -2
- package/src/container/Base.mjs +1 -1
- package/src/core/Base.mjs +18 -2
- package/src/date/DayViewComponent.mjs +2 -2
- package/src/date/SelectorContainer.mjs +1 -1
- package/src/form/field/CheckBox.mjs +4 -4
- package/src/form/field/ComboBox.mjs +6 -1
- package/src/form/field/FileUpload.mjs +25 -39
- package/src/form/field/Range.mjs +1 -1
- package/src/form/field/Text.mjs +3 -3
- package/src/form/field/TextArea.mjs +2 -3
- package/src/grid/Body.mjs +6 -2
- package/src/list/Color.mjs +2 -2
- package/src/main/DeltaUpdates.mjs +157 -98
- package/src/main/addon/AmCharts.mjs +53 -73
- package/src/main/addon/Base.mjs +11 -0
- package/src/main/addon/MonacoEditor.mjs +31 -58
- package/src/manager/ClassHierarchy.mjs +114 -0
- package/src/menu/List.mjs +1 -1
- package/src/plugin/Popover.mjs +2 -2
- package/src/sitemap/Component.mjs +1 -1
- package/src/table/Body.mjs +6 -2
- package/src/tooltip/Base.mjs +1 -6
- package/src/tree/Accordion.mjs +3 -3
- package/src/vdom/Helper.mjs +21 -19
- package/src/worker/App.mjs +1 -2
- package/src/worker/Base.mjs +6 -4
- package/src/worker/Canvas.mjs +2 -3
- package/src/worker/Data.mjs +5 -7
- package/src/worker/Task.mjs +2 -3
- package/src/worker/VDom.mjs +3 -4
- package/src/worker/mixin/RemoteMethodAccess.mjs +4 -1
- package/learn/guides/MainThreadAddonExample.md +0 -15
- package/learn/guides/MainThreadAddonIntro.md +0 -44
@@ -0,0 +1,280 @@
|
|
1
|
+
|
2
|
+
**Pre-requisite:** It is highly recommended to study [The Unified Class Config System](#/learn/benefits.ConfigSystem)
|
3
|
+
first to understand the foundational concepts and benefits.
|
4
|
+
|
5
|
+
The Neo.mjs class configuration system is a cornerstone of the framework, providing a powerful, declarative, and
|
6
|
+
reactive way to manage the state of your components and classes. Its internal mechanics are deeply intertwined with
|
7
|
+
the instance lifecycle, ensuring predictable and consistent behavior. This guide will take you on a deep dive into
|
8
|
+
how it achieves its remarkable consistency and power.
|
9
|
+
|
10
|
+
## 1. Core Concepts Recap
|
11
|
+
|
12
|
+
At its heart, the config system is built on a few key principles:
|
13
|
+
|
14
|
+
* **`static config` Block:** All configurable properties of a class are declared in a `static config = {}` block.
|
15
|
+
This provides a single, clear source of truth for a class's API.
|
16
|
+
* **`_` Suffix Convention:** Config properties that require custom logic when they change are declared with a trailing
|
17
|
+
underscore (e.g., `myValue_`). This signals the framework to automatically create a native getter and setter on the
|
18
|
+
class's prototype for this property.
|
19
|
+
* **Lifecycle Hooks:** For a config like `myValue_`, the framework provides optional lifecycle hooks that you can
|
20
|
+
implement in your class:
|
21
|
+
* `beforeGetMyValue(value)`: Called before the getter returns the value.
|
22
|
+
* `beforeSetMyValue(value, oldValue)`: Called before the setter applies the new value.
|
23
|
+
* `afterSetMyValue(value, oldValue)`: Called after the setter has applied the new value.
|
24
|
+
* **Reactivity:** The `afterSet` hooks are the heart of the reactive system. They allow you to define logic that
|
25
|
+
automatically runs whenever a specific config property changes, ensuring your UI and application state are always
|
26
|
+
in sync.
|
27
|
+
|
28
|
+
## 2. The Internal Mechanics: `set()`, `processConfigs()`, and `configSymbol`
|
29
|
+
|
30
|
+
To truly understand how Neo.mjs handles complex scenarios like simultaneous updates and inter-dependencies, we must
|
31
|
+
look at the internal machinery: the `set()` and `processConfigs()` methods in `Neo.core.Base`, and the special
|
32
|
+
`configSymbol` object.
|
33
|
+
|
34
|
+
### The `set()` Method: Your Gateway to Updates
|
35
|
+
|
36
|
+
The `set()` method is the public interface for changing one or more config properties at once. When you call
|
37
|
+
`this.set({a: 1, b: 2})`, you kick off a carefully orchestrated sequence.
|
38
|
+
|
39
|
+
[[Source: core.Base.mjs](https://github.com/neomjs/neo/blob/dev/src/core/Base.mjs)]
|
40
|
+
```javascript readonly
|
41
|
+
// Simplified for clarity
|
42
|
+
set(values={}) {
|
43
|
+
let me = this;
|
44
|
+
|
45
|
+
// If there are pending configs from a previous operation, process them first.
|
46
|
+
if (Object.keys(me[configSymbol]).length > 0) {
|
47
|
+
me.processConfigs();
|
48
|
+
}
|
49
|
+
|
50
|
+
// Stage the new values in the configSymbol object.
|
51
|
+
Object.assign(me[configSymbol], values); // (A)
|
52
|
+
|
53
|
+
// Start processing the newly staged values.
|
54
|
+
me.processConfigs(true); // (B)
|
55
|
+
}
|
56
|
+
```
|
57
|
+
|
58
|
+
Here’s the breakdown:
|
59
|
+
1. **Pre-processing (within `construct()`):** The method first checks if the internal `configSymbol` object has any
|
60
|
+
leftover configs from a previous, unfinished operation (e.g., from a parent class's `construct()` call). If so,
|
61
|
+
it processes them to ensure a clean state before new values are staged.
|
62
|
+
2. **Staging (A):** `Object.assign(me[configSymbol], values)` is the critical first step. All new values from your
|
63
|
+
`set()` call are merged into the `configSymbol` object. This object acts as a **temporary staging area**. It
|
64
|
+
creates a snapshot of the intended end-state for all properties in this specific `set()` operation *before*
|
65
|
+
any individual setters or `afterSet` hooks are invoked.
|
66
|
+
3. **Processing (B):** `me.processConfigs(true)` is called. This kicks off the process of applying the staged values
|
67
|
+
from `configSymbol` to the actual instance properties. The `true` argument (`forceAssign`) is crucial, as we'll
|
68
|
+
see next.
|
69
|
+
|
70
|
+
### The `processConfigs()` Method: The Heart of the Operation
|
71
|
+
|
72
|
+
This internal method iteratively processes the configs stored in `configSymbol`. It's designed as a recursive
|
73
|
+
function to handle the dynamic nature of config processing, where one `afterSet` might trigger another `set()`.
|
74
|
+
|
75
|
+
[[Source: core.Base.mjs](https://github.com/neomjs/neo/blob/dev/src/core/Base.mjs)]
|
76
|
+
```javascript readonly
|
77
|
+
// Simplified for clarity
|
78
|
+
processConfigs(forceAssign=false) {
|
79
|
+
let me = this,
|
80
|
+
keys = Object.keys(me[configSymbol]); // Get keys of pending configs
|
81
|
+
|
82
|
+
if (keys.length > 0) {
|
83
|
+
let key = keys[0];
|
84
|
+
let value = me[configSymbol][key];
|
85
|
+
|
86
|
+
// The auto-generated setter for the config is triggered here.
|
87
|
+
me[key] = value; // (C)
|
88
|
+
|
89
|
+
// The config is removed from the staging area after its setter is called.
|
90
|
+
delete me[configSymbol][key]; // (D)
|
91
|
+
|
92
|
+
// Recursively call to process the next config.
|
93
|
+
me.processConfigs(forceAssign); // (E)
|
94
|
+
}
|
95
|
+
}
|
96
|
+
```
|
97
|
+
|
98
|
+
* **Iteration:** `processConfigs` takes the *first* key from `configSymbol`. It avoids a standard loop to prevent
|
99
|
+
issues if an `afterSet` hook modifies `configSymbol`.
|
100
|
+
* **Assignment (C):** `me[key] = value` is the most important step. This does **not** directly change a backing
|
101
|
+
field. Instead, it triggers the actual auto-generated **setter** for the config property (e.g., `set a(value)`).
|
102
|
+
This native setter is responsible for:
|
103
|
+
1. Running the `beforeSet` hook (if it exists).
|
104
|
+
2. Updating the internal backing property (e.g., `this._a = value`).
|
105
|
+
3. Running the `afterSet` hook (if it exists and the value has changed).
|
106
|
+
* **Deletion (D):** `delete me[configSymbol][key]` removes the property from the staging area *after* its setter
|
107
|
+
has been invoked. This is vital to prevent reprocessing and to mark the config as handled.
|
108
|
+
* **Recursion (E):** The method calls itself to process the next item in `configSymbol` until it's empty.
|
109
|
+
|
110
|
+
## 3. Solving the "Circular Reference" Problem
|
111
|
+
|
112
|
+
What happens when two `afterSet` methods depend on each other's properties?
|
113
|
+
|
114
|
+
Consider this common scenario:
|
115
|
+
```javascript readonly
|
116
|
+
class MyComponent extends Component {
|
117
|
+
static config = {
|
118
|
+
a_: 1,
|
119
|
+
b_: 2
|
120
|
+
}
|
121
|
+
|
122
|
+
afterSetA(value, oldValue) {
|
123
|
+
// This depends on 'b'
|
124
|
+
console.log(`a changed to ${value}, b is ${this.b}`);
|
125
|
+
}
|
126
|
+
|
127
|
+
afterSetB(value, oldValue) {
|
128
|
+
// This depends on 'a'
|
129
|
+
console.log(`b changed to ${value}, a is ${this.a}`);
|
130
|
+
}
|
131
|
+
|
132
|
+
onConstructed() {
|
133
|
+
super.onConstructed();
|
134
|
+
this.set({
|
135
|
+
a: 10,
|
136
|
+
b: 20
|
137
|
+
});
|
138
|
+
}
|
139
|
+
}
|
140
|
+
```
|
141
|
+
When `this.set({a: 10, b: 20})` is called, which `afterSet` runs first? And when it runs, what value will it see
|
142
|
+
for the *other* property?
|
143
|
+
|
144
|
+
**This is where the brilliance of the `configSymbol` shines.**
|
145
|
+
|
146
|
+
Here's the sequence:
|
147
|
+
1. **`set()` called:** `this.set({a: 10, b: 20})` is executed.
|
148
|
+
2. **Staging:** The `configSymbol` is immediately populated: `me[configSymbol] = {a: 10, b: 20}`. The internal
|
149
|
+
backing properties `_a` and `_b` have **not** been updated yet.
|
150
|
+
3. **`processConfigs()` starts:**
|
151
|
+
* It picks `a`. The setter `setA(10)` is called.
|
152
|
+
* Inside `setA`, the internal `this._a` is updated to `10`.
|
153
|
+
* `afterSetA(10, 1)` is triggered.
|
154
|
+
4. **Inside `afterSetA`:**
|
155
|
+
* The code encounters `this.b`. This calls the auto-generated getter for `b`.
|
156
|
+
* **Crucially, the getter for `b` is smart.** It first checks if `b` exists as a key in the `configSymbol`
|
157
|
+
staging area.
|
158
|
+
* It finds `b: 20` in `configSymbol` and immediately returns `20`, the **new, pending value**. It does *not*
|
159
|
+
return the old value from `this._b`.
|
160
|
+
* The console logs: `a changed to 10, b is 20`.
|
161
|
+
5. **`processConfigs()` continues:**
|
162
|
+
* `a` is removed from `configSymbol`.
|
163
|
+
* The recursion continues, and it now picks `b`. The setter `setB(20)` is called.
|
164
|
+
* Inside `setB`, `this._b` is updated to `20`.
|
165
|
+
* `afterSetB(20, 2)` is triggered.
|
166
|
+
6. **Inside `afterSetB`:**
|
167
|
+
* The code encounters `this.a`. The getter for `a` is called.
|
168
|
+
* It checks `configSymbol`, but `a` is no longer there (it was processed).
|
169
|
+
* It therefore returns the value from the internal backing property, `this._a`, which is now `10`.
|
170
|
+
* The console logs: `b changed to 20, a is 10`.
|
171
|
+
|
172
|
+
**Conclusion:** The `configSymbol` acts as a consistent, authoritative snapshot for the duration of a `set()`
|
173
|
+
operation. This guarantees that all `afterSet` handlers, regardless of their execution order, operate on the most
|
174
|
+
current and consistent state of all config properties involved in that operation.
|
175
|
+
|
176
|
+
## 4. In-depth Example: A Reactive `MainContainer`
|
177
|
+
|
178
|
+
Let's analyze a practical example to see these concepts in action. The `Neo.examples.core.config.MainContainer`
|
179
|
+
demonstrates how to build a reactive UI declaratively.
|
180
|
+
|
181
|
+
**The Goal:** Create a container with two labels. The text of each label is calculated based on the values of two
|
182
|
+
config properties, `a` and `b`. A button allows the user to change `a` and `b` simultaneously.
|
183
|
+
|
184
|
+
**The Declarative Approach (`static config`)**
|
185
|
+
|
186
|
+
The entire UI structure, including child components and event handlers, is defined within the `static config` block.
|
187
|
+
This is the recommended approach as it makes the component's structure immediately clear.
|
188
|
+
|
189
|
+
```javascript readonly
|
190
|
+
// From: Neo.examples.core.config.MainContainer
|
191
|
+
import Panel from '../../../src/container/Panel.mjs';
|
192
|
+
import Viewport from '../../../src/container/Viewport.mjs';
|
193
|
+
|
194
|
+
class MainContainer extends Viewport {
|
195
|
+
static config = {
|
196
|
+
className: 'Neo.examples.core.config.MainContainer',
|
197
|
+
a_: null,
|
198
|
+
b_: null,
|
199
|
+
style : { padding: '20px' },
|
200
|
+
items: [{
|
201
|
+
module: Panel,
|
202
|
+
// ... panel configs
|
203
|
+
headers: [{
|
204
|
+
dock : 'top',
|
205
|
+
items: [
|
206
|
+
{ ntype: 'label', flag: 'label1' },
|
207
|
+
{ ntype: 'label', flag: 'label2' },
|
208
|
+
{ ntype: 'component', flex: 1 },
|
209
|
+
{
|
210
|
+
handler: 'up.changeConfig', // Declarative handler
|
211
|
+
iconCls: 'fa fa-user',
|
212
|
+
text : 'Change configs'
|
213
|
+
}
|
214
|
+
]
|
215
|
+
}],
|
216
|
+
items: [{ ntype: 'label', text: 'Click the change configs button!' }]
|
217
|
+
}]
|
218
|
+
}
|
219
|
+
|
220
|
+
onConstructed() {
|
221
|
+
super.onConstructed();
|
222
|
+
this.set({ a: 5, b: 5 });
|
223
|
+
}
|
224
|
+
|
225
|
+
afterSetA(value, oldValue) {
|
226
|
+
if (oldValue !== undefined) {
|
227
|
+
this.down({flag: 'label1'}).text = value + this.b;
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
afterSetB(value, oldValue) {
|
232
|
+
if (oldValue !== undefined) {
|
233
|
+
this.down({flag: 'label2'}).text = value + this.a;
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
changeConfig(data) {
|
238
|
+
this.set({ a: 10, b: 10 });
|
239
|
+
}
|
240
|
+
}
|
241
|
+
```
|
242
|
+
|
243
|
+
### Tracing the Data Flow
|
244
|
+
|
245
|
+
1. **Initialization (`onConstructed`)**:
|
246
|
+
* `this.set({a: 5, b: 5})` is called. This happens within the `onConstructed()` lifecycle hook, which is guaranteed
|
247
|
+
to run *after* the instance's `construct()` method has fully processed its initial configuration.
|
248
|
+
* `configSymbol` becomes `{a: 5, b: 5}`.
|
249
|
+
* `afterSetA` runs. It calculates `label1.text` as `value (5) + this.b (reads 5 from configSymbol) = 10`.
|
250
|
+
* `afterSetB` runs. It calculates `label2.text` as `value (5) + this.a (reads 5 from _a) = 10`.
|
251
|
+
* **Initial State:** `label1` shows "10", `label2` shows "10".
|
252
|
+
|
253
|
+
2. **Button Click (`changeConfig`)**:
|
254
|
+
* The button's `handler: 'up.changeConfig'` finds and calls the `changeConfig` method on the `MainContainer`.
|
255
|
+
* `this.set({a: 10, b: 10})` is called.
|
256
|
+
* `configSymbol` becomes `{a: 10, b: 10}`.
|
257
|
+
* `afterSetA` runs. It calculates `label1.text` as `value (10) + this.b (reads 10 from configSymbol) = 20`.
|
258
|
+
* `afterSetB` runs. It calculates `label2.text` as `value (10) + this.a (reads 10 from _a) = 20`.
|
259
|
+
* **New State:** `label1` shows "20", `label2` shows "20".
|
260
|
+
|
261
|
+
This example vividly demonstrates the dynamic and reactive nature of the system, where a single declarative state
|
262
|
+
change automatically propagates through the component logic.
|
263
|
+
|
264
|
+
## 5. Best Practices
|
265
|
+
|
266
|
+
* **Embrace Declarativity:** Define your entire UI structure inside `static config` whenever possible. This improves
|
267
|
+
readability and maintainability.
|
268
|
+
* **Use the `_` Suffix Wisely:** Only add the trailing underscore to configs that need `afterSet`, `beforeSet` or
|
269
|
+
`beforeGet` based logic. For simple value properties, omit it to avoid unnecessary overhead.
|
270
|
+
* **Keep `afterSet` Handlers Pure:** An `afterSet` handler should ideally only react to the change of its own
|
271
|
+
property and update other parts of the application. Avoid triggering complex chains of `set()` calls from within
|
272
|
+
an `afterSet` if possible.
|
273
|
+
* **Batch Updates with `set()`:** When you need to change multiple properties at once, always use a single
|
274
|
+
`set({a: 1, b: 2})` call. This is more efficient and ensures consistency, as demonstrated above.
|
275
|
+
* **Use `onConstructed` for Post-Construction Logic:** Use the `onConstructed` lifecycle method to perform any setup
|
276
|
+
that depends on the instance's initial configuration being fully processed. This is the ideal place for logic that
|
277
|
+
requires all configs to be set and potentially other instances to be created (if set-driven).
|
278
|
+
|
279
|
+
By understanding these internal mechanics and following best practices, you can leverage the full power of Neo.mjs's
|
280
|
+
class config system to build highly complex, reactive, and maintainable applications with confidence.
|
@@ -1,45 +1,287 @@
|
|
1
1
|
## Introduction
|
2
2
|
|
3
|
-
Neo.mjs is
|
3
|
+
A major strength of Neo.mjs is its extensive library of components. In most cases, you can build sophisticated
|
4
|
+
user interfaces simply by creating configuration objects for these existing components and adding them to a container's
|
5
|
+
`items` array. This configuration-driven approach is a significant departure from frameworks like Angular, React, or
|
6
|
+
Vue, where creating custom components is a core part of the development workflow.
|
4
7
|
|
8
|
+
However, there are times when you need to create something truly unique or encapsulate a specific set of configurations
|
9
|
+
and logic for reuse. In these scenarios, creating a custom component by extending a framework class is the perfect
|
10
|
+
solution.
|
5
11
|
|
6
|
-
|
12
|
+
This guide will walk you through the process.
|
7
13
|
|
8
|
-
##
|
14
|
+
## Choosing the Right Base Class
|
9
15
|
|
10
|
-
|
16
|
+
In the world of React, developers often use Higher-Order Components (HOCs) to reuse component logic. In Neo.mjs, you
|
17
|
+
achieve a similar result through class extension. The key to creating a robust and efficient custom component is
|
18
|
+
choosing the correct base class to extend.
|
19
|
+
|
20
|
+
Instead of extending the most generic `Neo.component.Base` class, look for a more specialized class that already
|
21
|
+
provides the functionality you need.
|
22
|
+
|
23
|
+
- If your component needs to contain other components, extend `Neo.container.Base`.
|
24
|
+
- If you're creating an interactive element, extending `Neo.button.Base` gives you focus and keyboard support.
|
25
|
+
- If you need a custom form field, look for a suitable class within `Neo.form.field`.
|
26
|
+
|
27
|
+
By choosing the most specific base class, you inherit a rich set of features, saving you from having to reinvent the
|
28
|
+
wheel and ensuring your component integrates smoothly into the framework.
|
29
|
+
|
30
|
+
## Real-World Examples inside the Neo.mjs Component Library
|
31
|
+
|
32
|
+
The Neo.mjs framework itself uses this principle of extending the most specific class. Let's look at a couple of
|
33
|
+
examples from the framework's source code.
|
34
|
+
|
35
|
+
### Toolbar Inheritance
|
36
|
+
|
37
|
+
- **`Neo.toolbar.Base`** extends `Neo.container.Base`.
|
38
|
+
It's the foundational toolbar and extends `Container` because its main purpose is to hold other components. It adds
|
39
|
+
features like docking.
|
40
|
+
|
41
|
+
- **`Neo.tab.header.Toolbar`** extends `Neo.toolbar.Base`.
|
42
|
+
This is a specialized toolbar for tab headers. It inherits the ability to hold items and be docked, and adds new
|
43
|
+
logic for managing the active tab indicator.
|
44
|
+
|
45
|
+
- **`Neo.grid.header.Toolbar`** extends `Neo.toolbar.Base`.
|
46
|
+
This toolbar is for grid headers. It also inherits from `toolbar.Base` and adds grid-specific features like column
|
47
|
+
resizing and reordering.
|
48
|
+
|
49
|
+
### Button Inheritance
|
50
|
+
|
51
|
+
- **`Neo.button.Base`** extends `Neo.component.Base`.
|
52
|
+
This is the basic button, providing core features like click handling and icon support.
|
53
|
+
|
54
|
+
- **`Neo.tab.header.Button`** extends `Neo.button.Base`.
|
55
|
+
A button used in tab headers. It inherits all the standard button features and adds a visual indicator for the
|
56
|
+
active tab.
|
57
|
+
|
58
|
+
- **`Neo.grid.header.Button`** extends `Neo.button.Base`.
|
59
|
+
A button for grid column headers. It inherits from the base button and adds features for sorting and filtering the
|
60
|
+
grid data.
|
61
|
+
|
62
|
+
These examples show how building on top of specialized base classes leads to a clean, maintainable, and powerful
|
63
|
+
component architecture.
|
64
|
+
|
65
|
+
## The Role of `Neo.setupClass()` and the Global `Neo` Namespace
|
66
|
+
|
67
|
+
When you define a class in Neo.mjs and pass it to `Neo.setupClass()`, the framework does more than just process its configurations and apply mixins. A crucial step performed by `Neo.setupClass()` is to **enhance the global `Neo` namespace** with a reference to your newly defined class.
|
68
|
+
|
69
|
+
This means that after `Neo.setupClass(MyClass)` is executed, your class becomes accessible globally via `Neo.[your.class.name]`, where `[your.class.name]` corresponds to the `className` config you defined (e.g., `Neo.button.Base`, `Neo.form.field.Text`).
|
70
|
+
|
71
|
+
**Implications for Class Extension and Usage:**
|
72
|
+
|
73
|
+
* **Global Accessibility**: You can refer to any framework class (or your own custom classes after they've been set up) using their full `Neo` namespace path (e.g., `Neo.button.Base`, `Neo.container.Base`) anywhere in your application code, even without an explicit ES module import for that specific class.
|
74
|
+
* **Convenience vs. Best Practice**: While `extends Neo.button.Base` might technically work without an `import Button from '...'`, it is generally **not recommended** for application code. Explicit ES module imports (e.g., `import Button from '../button/Base.mjs';`) are preferred because they:
|
75
|
+
* **Improve Readability**: Clearly show the dependencies of your module.
|
76
|
+
* **Enhance Tooling**: Enable better static analysis, auto-completion, and refactoring support in modern IDEs.
|
77
|
+
* **Ensure Consistency**: Promote a consistent and predictable coding style.
|
78
|
+
* **Framework Internal Use**: The global `Neo` namespace is heavily utilized internally by the framework itself for its class registry, dependency resolution, and dynamic instantiation (e.g., when using `ntype` or `module` configs).
|
79
|
+
|
80
|
+
Understanding this mechanism clarifies how Neo.mjs manages its class system and provides the underlying flexibility for its configuration-driven approach.
|
81
|
+
|
82
|
+
## Overriding Ancestor Configs
|
83
|
+
|
84
|
+
The simplest way to create a custom component is to extend an existing one and override some of its default
|
85
|
+
configuration values.
|
86
|
+
|
87
|
+
Every class in Neo.mjs has a `static config` block where its properties are defined. When you extend a class, you can
|
88
|
+
define your own `static config` block and set new default values for any property inherited from an ancestor class.
|
89
|
+
|
90
|
+
In the example below, we create `MySpecialButton` by extending `Neo.button.Base`. We then override the `iconCls` and
|
91
|
+
`ui` configs to create a button with a specific look and feel.
|
92
|
+
|
93
|
+
## Introducing New Configs
|
94
|
+
|
95
|
+
You can also add entirely new configuration properties to your custom components. To make a config "reactive" – meaning
|
96
|
+
it automatically triggers a lifecycle method when its value changes – you **must** define it with a trailing underscore (`_`).
|
97
|
+
|
98
|
+
For a reactive config like `myConfig_`, the framework provides this behavior:
|
99
|
+
- **Reading**: You can access the value directly: `this.myConfig`.
|
100
|
+
- **Writing**: Assigning a new value (`this.myConfig = 'new value'`) triggers a prototype-based setter. This is the core of Neo.mjs reactivity.
|
101
|
+
- **Hooks**: The framework provides three optional hooks for each reactive config: `beforeGet`, `beforeSet`, and `afterSet`. After a value is set, the `afterSetMyConfig(value, oldValue)` method is automatically called.
|
102
|
+
|
103
|
+
If you define a config without the trailing underscore, it will simply be a static property on the class instance and will not trigger any lifecycle methods.
|
104
|
+
|
105
|
+
For a complete explanation of the config system, including details on all the lifecycle hooks, please see the [Unified Config System guide](benefits.ConfigSystem).
|
106
|
+
|
107
|
+
## Example: A Custom Button
|
108
|
+
|
109
|
+
Let's look at a practical example. Here, we'll create a custom button that combines the standard `text` config with a new
|
110
|
+
`specialText_` config to create a dynamic label.
|
11
111
|
|
12
112
|
```javascript live-preview
|
13
|
-
import Button
|
14
|
-
|
113
|
+
import Button from '../button/Base.mjs';
|
114
|
+
import Container from '../container/Base.mjs';
|
115
|
+
|
116
|
+
// 1. Define our custom component by extending a framework class.
|
15
117
|
class MySpecialButton extends Button {
|
16
118
|
static config = {
|
17
119
|
className: 'Example.view.MySpecialButton',
|
18
|
-
|
19
|
-
|
120
|
+
|
121
|
+
// a. Override configs from the parent class
|
122
|
+
iconCls: 'far fa-face-grin-wide',
|
123
|
+
ui : 'ghost',
|
124
|
+
|
125
|
+
// b. Add a new reactive config (note the trailing underscore)
|
126
|
+
specialText_: 'I am special'
|
127
|
+
}
|
128
|
+
|
129
|
+
// c. Hook into the component lifecycle
|
130
|
+
afterSetSpecialText(value, oldValue) {
|
131
|
+
this.updateButtonText()
|
132
|
+
}
|
133
|
+
|
134
|
+
afterSetText(value, oldValue) {
|
135
|
+
this.updateButtonText()
|
136
|
+
}
|
137
|
+
|
138
|
+
// d. A custom method to update the button's text
|
139
|
+
updateButtonText() {
|
140
|
+
const {specialText, text} = this;
|
141
|
+
let fullText = `${text} (${specialText})`;
|
142
|
+
|
143
|
+
// Directly manipulate the VDom text node and update the component
|
144
|
+
this.textNode.text = fullText;
|
145
|
+
this.update();
|
20
146
|
}
|
21
147
|
}
|
22
148
|
|
23
149
|
MySpecialButton = Neo.setupClass(MySpecialButton);
|
24
150
|
|
25
151
|
|
26
|
-
|
27
|
-
|
152
|
+
// 2. Use the new component in a view.
|
28
153
|
class MainView extends Container {
|
29
154
|
static config = {
|
30
155
|
className: 'Example.view.MainView',
|
31
|
-
layout : {ntype:'vbox', align:'start'},
|
156
|
+
layout : {ntype: 'vbox', align: 'start'},
|
32
157
|
items : [{
|
158
|
+
// A standard framework button for comparison
|
33
159
|
module : Button,
|
34
160
|
iconCls: 'fa fa-home',
|
35
161
|
text : 'A framework button'
|
36
162
|
}, {
|
37
|
-
|
38
|
-
|
163
|
+
// Our new custom button
|
164
|
+
module : MySpecialButton,
|
165
|
+
text : 'My button',
|
166
|
+
specialText: 'is very special'
|
167
|
+
}]
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
MainView = Neo.setupClass(MainView);
|
172
|
+
```
|
173
|
+
|
174
|
+
### Breakdown of the Example:
|
175
|
+
|
176
|
+
1. **Class Definition**: We define `MySpecialButton` which `extends` the framework's `Button` class.
|
177
|
+
2. **New Reactive Config**: We add a `specialText_` config. The trailing underscore makes it reactive.
|
178
|
+
3. **Lifecycle Methods**: We implement `afterSetSpecialText()` and override `afterSetText()` to call our custom
|
179
|
+
`updateButtonText()` method. Because `afterSet` hooks are called for initial values upon instantiation, this
|
180
|
+
ensures the button text is correct from the start and stays in sync.
|
181
|
+
4. **Custom Method**: The `updateButtonText()` method combines the `text` and `specialText` configs and updates the
|
182
|
+
`text` property of the button's `textNode` in the VDOM.
|
183
|
+
5. **`this.update()`**: After changing the VDOM, we call `this.update()` to make the framework apply our changes to the
|
184
|
+
real DOM.
|
185
|
+
|
186
|
+
This example shows how you can create a component that encapsulates its own logic and provides a richer, more dynamic
|
187
|
+
behavior than a standard component.
|
188
|
+
|
189
|
+
## Extending `Component.Base`: Building VDom from Scratch
|
190
|
+
|
191
|
+
While extending specialized components like `Button` or `Container` is common for adding features (acting like a
|
192
|
+
Higher-Order Component), there are times when you need to define a component's HTML structure from the ground up. For
|
193
|
+
this, you extend the generic `Neo.component.Base`.
|
194
|
+
|
195
|
+
When you extend `component.Base`, you are responsible for defining the component's entire virtual DOM (VDom) structure
|
196
|
+
using the `vdom` config. This gives you complete control over the rendered output.
|
197
|
+
|
198
|
+
### Example: A Simple Profile Badge
|
199
|
+
|
200
|
+
Let's create a `ProfileBadge` component that displays a user's name and an online status indicator.
|
201
|
+
|
202
|
+
```javascript live-preview
|
203
|
+
import Component from '../component/Base.mjs';
|
204
|
+
import Container from '../container/Base.mjs';
|
205
|
+
import NeoArray from '../util/Array.mjs';
|
206
|
+
import VdomUtil from '../util/Vdom.mjs';
|
207
|
+
|
208
|
+
// 1. Extend the generic Component.Base
|
209
|
+
class ProfileBadge extends Component {
|
210
|
+
static config = {
|
211
|
+
className: 'Example.view.ProfileBadge',
|
212
|
+
ntype : 'profile-badge',
|
213
|
+
|
214
|
+
// a. Define the VDom from scratch
|
215
|
+
vdom: {
|
216
|
+
cls: ['profile-badge'],
|
217
|
+
cn : [
|
218
|
+
{tag: 'span', cls: ['status-indicator'], flag: 'statusNode'},
|
219
|
+
{tag: 'span', cls: ['username'], flag: 'usernameNode'}
|
220
|
+
]
|
221
|
+
},
|
222
|
+
|
223
|
+
// b. Add new reactive configs to control the component (note the trailing underscore)
|
224
|
+
online_ : false,
|
225
|
+
username_: 'Guest'
|
226
|
+
}
|
227
|
+
|
228
|
+
// d. Define getters for easy access to flagged VDom nodes
|
229
|
+
get statusNode() {
|
230
|
+
return VdomUtil.getByFlag(this.vdom, 'statusNode')
|
231
|
+
}
|
232
|
+
|
233
|
+
get usernameNode() {
|
234
|
+
return VdomUtil.getByFlag(this.vdom, 'usernameNode')
|
235
|
+
}
|
236
|
+
|
237
|
+
// c. Use lifecycle methods to react to config changes
|
238
|
+
afterSetOnline(value, oldValue) {
|
239
|
+
// Access the VDom node via the getter
|
240
|
+
NeoArray.toggle(this.statusNode.cls, 'online', value);
|
241
|
+
this.update() // Trigger a VDom update
|
242
|
+
}
|
243
|
+
|
244
|
+
afterSetUsername(value, oldValue) {
|
245
|
+
this.usernameNode.text = value;
|
246
|
+
this.update()
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
ProfileBadge = Neo.setupClass(ProfileBadge);
|
251
|
+
|
252
|
+
|
253
|
+
// 2. Use the new component
|
254
|
+
class MainView extends Container {
|
255
|
+
static config = {
|
256
|
+
className: 'Example.view.MainView',
|
257
|
+
layout : {ntype: 'vbox', align: 'start'},
|
258
|
+
items : [{
|
259
|
+
module : ProfileBadge,
|
260
|
+
username: 'Alice',
|
261
|
+
online : true
|
262
|
+
}, {
|
263
|
+
module : ProfileBadge,
|
264
|
+
username: 'Bob',
|
265
|
+
online : false,
|
266
|
+
style : {marginTop: '10px'}
|
39
267
|
}]
|
40
268
|
}
|
41
269
|
}
|
42
270
|
|
43
|
-
Neo.setupClass(MainView);
|
271
|
+
MainView= Neo.setupClass(MainView);
|
44
272
|
```
|
45
273
|
|
274
|
+
### Key Differences in this Approach:
|
275
|
+
|
276
|
+
1. **Base Class**: We extend `Neo.component.Base` because we are not inheriting complex logic like a `Button` or
|
277
|
+
`Container`.
|
278
|
+
2. **`vdom` Config**: We define the entire HTML structure inside the `vdom` config. We use `flag`s (`statusNode`,
|
279
|
+
`usernameNode`) to easily reference these VDom nodes later.
|
280
|
+
3. **Lifecycle Methods**: We use `afterSet...` methods to react to changes in our custom `online_` and `username_`
|
281
|
+
configs. Inside these methods, we directly manipulate the properties of our VDom nodes and then call `this.update()`
|
282
|
+
to apply the changes to the real DOM.
|
283
|
+
|
284
|
+
This approach gives you maximum control, but it also means you are responsible for building the structure yourself.
|
285
|
+
|
286
|
+
For a deeper dive into advanced VDom manipulation, including performance best practices and security, please refer to the
|
287
|
+
[Working with VDom guide](guides.WorkingWithVDom).
|