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.
Files changed (149) hide show
  1. package/ServiceWorker.mjs +2 -2
  2. package/apps/colors/view/GridContainer.mjs +1 -1
  3. package/apps/covid/view/AttributionComponent.mjs +1 -1
  4. package/apps/covid/view/HeaderContainer.mjs +6 -6
  5. package/apps/covid/view/MainContainerController.mjs +5 -5
  6. package/apps/covid/view/TableContainerController.mjs +1 -1
  7. package/apps/covid/view/country/Gallery.mjs +13 -13
  8. package/apps/covid/view/country/Helix.mjs +13 -13
  9. package/apps/covid/view/country/HistoricalDataTable.mjs +1 -1
  10. package/apps/email/view/Viewport.mjs +2 -2
  11. package/apps/form/view/FormPageContainer.mjs +2 -3
  12. package/apps/form/view/SideNavList.mjs +1 -1
  13. package/apps/portal/index.html +1 -1
  14. package/apps/portal/resources/data/examples_dist_esm.json +1 -1
  15. package/apps/portal/resources/data/examples_dist_prod.json +2 -2
  16. package/apps/portal/view/HeaderToolbar.mjs +3 -3
  17. package/apps/portal/view/about/Container.mjs +2 -2
  18. package/apps/portal/view/about/MemberContainer.mjs +3 -3
  19. package/apps/portal/view/blog/List.mjs +7 -7
  20. package/apps/portal/view/examples/List.mjs +4 -4
  21. package/apps/portal/view/home/ContentBox.mjs +2 -2
  22. package/apps/portal/view/home/FeatureSection.mjs +3 -3
  23. package/apps/portal/view/home/FooterContainer.mjs +7 -7
  24. package/apps/portal/view/home/parts/AfterMath.mjs +3 -3
  25. package/apps/portal/view/home/parts/MainNeo.mjs +3 -3
  26. package/apps/portal/view/home/parts/References.mjs +6 -6
  27. package/apps/portal/view/learn/ContentComponent.mjs +18 -11
  28. package/apps/portal/view/learn/PageSectionsContainer.mjs +1 -1
  29. package/apps/portal/view/learn/PageSectionsList.mjs +2 -2
  30. package/apps/portal/view/services/Component.mjs +16 -16
  31. package/apps/realworld/view/FooterComponent.mjs +1 -1
  32. package/apps/realworld/view/HeaderComponent.mjs +8 -8
  33. package/apps/realworld/view/HomeComponent.mjs +6 -6
  34. package/apps/realworld/view/article/CommentComponent.mjs +4 -4
  35. package/apps/realworld/view/article/Component.mjs +14 -14
  36. package/apps/realworld/view/article/CreateCommentComponent.mjs +3 -3
  37. package/apps/realworld/view/article/CreateComponent.mjs +3 -3
  38. package/apps/realworld/view/article/PreviewComponent.mjs +1 -1
  39. package/apps/realworld/view/article/TagListComponent.mjs +2 -2
  40. package/apps/realworld/view/user/ProfileComponent.mjs +8 -8
  41. package/apps/realworld/view/user/SettingsComponent.mjs +4 -4
  42. package/apps/realworld/view/user/SignUpComponent.mjs +4 -4
  43. package/apps/realworld2/view/FooterComponent.mjs +1 -1
  44. package/apps/realworld2/view/HomeContainer.mjs +3 -3
  45. package/apps/realworld2/view/article/DetailsContainer.mjs +1 -1
  46. package/apps/realworld2/view/article/PreviewComponent.mjs +7 -7
  47. package/apps/realworld2/view/article/TagListComponent.mjs +2 -2
  48. package/apps/realworld2/view/user/ProfileContainer.mjs +1 -1
  49. package/apps/route/view/center/CardAdministration.mjs +2 -2
  50. package/apps/route/view/center/CardAdministrationDenied.mjs +1 -1
  51. package/apps/route/view/center/CardContact.mjs +2 -2
  52. package/apps/route/view/center/CardHome.mjs +1 -1
  53. package/apps/route/view/center/CardSection1.mjs +1 -1
  54. package/apps/route/view/center/CardSection2.mjs +1 -1
  55. package/apps/sharedcovid/view/AttributionComponent.mjs +1 -1
  56. package/apps/sharedcovid/view/HeaderContainer.mjs +6 -6
  57. package/apps/sharedcovid/view/MainContainerController.mjs +5 -5
  58. package/apps/sharedcovid/view/TableContainerController.mjs +1 -1
  59. package/apps/sharedcovid/view/country/Gallery.mjs +13 -13
  60. package/apps/sharedcovid/view/country/Helix.mjs +13 -13
  61. package/apps/sharedcovid/view/country/HistoricalDataTable.mjs +1 -1
  62. package/apps/shareddialog/childapps/shareddialog2/view/MainContainer.mjs +1 -1
  63. package/apps/shareddialog/view/MainContainer.mjs +1 -1
  64. package/buildScripts/createApp.mjs +2 -2
  65. package/learn/Glossary.md +261 -0
  66. package/learn/README.md +9 -14
  67. package/learn/benefits/ConfigSystem.md +536 -26
  68. package/learn/benefits/Effort.md +47 -2
  69. package/learn/benefits/Features.md +50 -32
  70. package/learn/benefits/FormsEngine.md +54 -24
  71. package/learn/benefits/MultiWindow.md +31 -5
  72. package/learn/benefits/Quick.md +45 -12
  73. package/learn/benefits/RPCLayer.md +75 -0
  74. package/learn/benefits/Speed.md +17 -12
  75. package/learn/guides/Collections.md +436 -0
  76. package/learn/guides/ConfigSystemDeepDive.md +280 -0
  77. package/learn/guides/CustomComponents.md +256 -14
  78. package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +17 -17
  79. package/learn/guides/ExtendingNeoClasses.md +331 -0
  80. package/learn/guides/Forms.md +449 -1
  81. package/learn/guides/InstanceLifecycle.md +295 -1
  82. package/learn/guides/Layouts.md +246 -1
  83. package/learn/guides/MainThreadAddons.md +475 -0
  84. package/learn/guides/Records.md +286 -0
  85. package/learn/guides/WorkingWithVDom.md +14 -14
  86. package/learn/guides/form_fields/ComboBox.md +241 -0
  87. package/learn/tree.json +57 -51
  88. package/package.json +2 -2
  89. package/resources/scss/src/apps/portal/learn/ContentComponent.scss +9 -0
  90. package/src/DefaultConfig.mjs +2 -2
  91. package/src/Main.mjs +8 -7
  92. package/src/Neo.mjs +16 -2
  93. package/src/button/Base.mjs +2 -2
  94. package/src/calendar/view/SettingsContainer.mjs +2 -2
  95. package/src/calendar/view/YearComponent.mjs +9 -9
  96. package/src/calendar/view/calendars/ColorsList.mjs +1 -1
  97. package/src/calendar/view/calendars/List.mjs +1 -1
  98. package/src/calendar/view/month/Component.mjs +15 -15
  99. package/src/calendar/view/week/Component.mjs +12 -12
  100. package/src/calendar/view/week/EventDragZone.mjs +4 -4
  101. package/src/calendar/view/week/TimeAxisComponent.mjs +3 -3
  102. package/src/component/Base.mjs +17 -2
  103. package/src/component/Carousel.mjs +2 -2
  104. package/src/component/Chip.mjs +3 -3
  105. package/src/component/Circle.mjs +2 -2
  106. package/src/component/DateSelector.mjs +8 -8
  107. package/src/component/Helix.mjs +1 -1
  108. package/src/component/Label.mjs +3 -18
  109. package/src/component/Legend.mjs +3 -3
  110. package/src/component/MagicMoveText.mjs +6 -14
  111. package/src/component/Process.mjs +3 -3
  112. package/src/component/Progress.mjs +1 -1
  113. package/src/component/StatusBadge.mjs +2 -2
  114. package/src/component/Timer.mjs +2 -2
  115. package/src/component/Toast.mjs +5 -3
  116. package/src/container/AccordionItem.mjs +2 -2
  117. package/src/container/Base.mjs +1 -1
  118. package/src/core/Base.mjs +18 -2
  119. package/src/date/DayViewComponent.mjs +2 -2
  120. package/src/date/SelectorContainer.mjs +1 -1
  121. package/src/form/field/CheckBox.mjs +4 -4
  122. package/src/form/field/ComboBox.mjs +6 -1
  123. package/src/form/field/FileUpload.mjs +25 -39
  124. package/src/form/field/Range.mjs +1 -1
  125. package/src/form/field/Text.mjs +3 -3
  126. package/src/form/field/TextArea.mjs +2 -3
  127. package/src/grid/Body.mjs +6 -2
  128. package/src/list/Color.mjs +2 -2
  129. package/src/main/DeltaUpdates.mjs +157 -98
  130. package/src/main/addon/AmCharts.mjs +53 -73
  131. package/src/main/addon/Base.mjs +11 -0
  132. package/src/main/addon/MonacoEditor.mjs +31 -58
  133. package/src/manager/ClassHierarchy.mjs +114 -0
  134. package/src/menu/List.mjs +1 -1
  135. package/src/plugin/Popover.mjs +2 -2
  136. package/src/sitemap/Component.mjs +1 -1
  137. package/src/table/Body.mjs +6 -2
  138. package/src/tooltip/Base.mjs +1 -6
  139. package/src/tree/Accordion.mjs +3 -3
  140. package/src/vdom/Helper.mjs +21 -19
  141. package/src/worker/App.mjs +1 -2
  142. package/src/worker/Base.mjs +6 -4
  143. package/src/worker/Canvas.mjs +2 -3
  144. package/src/worker/Data.mjs +5 -7
  145. package/src/worker/Task.mjs +2 -3
  146. package/src/worker/VDom.mjs +3 -4
  147. package/src/worker/mixin/RemoteMethodAccess.mjs +4 -1
  148. package/learn/guides/MainThreadAddonExample.md +0 -15
  149. 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 class-based, which means you're free to extend any component (or any other Neo.mjs class).
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
- ## Overriding ancestor configs
12
+ This guide will walk you through the process.
7
13
 
8
- ## Introducing new configs
14
+ ## Choosing the Right Base Class
9
15
 
10
- ## Lifecycle config properties
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 from '../button/Base.mjs';
14
- // In practice this would be some handy reusable component
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
- iconCls : 'far fa-face-grin-wide',
19
- ui : 'ghost'
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
- import Container from '../container/Base.mjs';
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
- module : MySpecialButton,
38
- text : 'My special button'
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).