neo.mjs 10.0.0-beta.2 → 10.0.0-beta.4

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 (52) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/form/view/FormPageContainer.mjs +2 -3
  4. package/apps/portal/index.html +1 -1
  5. package/apps/portal/view/ViewportController.mjs +1 -1
  6. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  7. package/apps/portal/view/learn/ContentComponent.mjs +18 -11
  8. package/apps/portal/view/learn/MainContainerController.mjs +6 -6
  9. package/learn/README.md +9 -14
  10. package/learn/guides/datahandling/Collections.md +436 -0
  11. package/learn/guides/datahandling/Grids.md +621 -0
  12. package/learn/guides/datahandling/Records.md +287 -0
  13. package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +145 -1
  14. package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
  15. package/learn/guides/uibuildingblocks/CustomComponents.md +287 -0
  16. package/learn/guides/uibuildingblocks/Layouts.md +248 -0
  17. package/learn/guides/userinteraction/Forms.md +449 -0
  18. package/learn/guides/userinteraction/form_fields/ComboBox.md +241 -0
  19. package/learn/tree.json +63 -52
  20. package/package.json +2 -2
  21. package/resources/scss/src/apps/portal/learn/ContentComponent.scss +9 -0
  22. package/src/DefaultConfig.mjs +2 -2
  23. package/src/Neo.mjs +37 -29
  24. package/src/collection/Base.mjs +29 -2
  25. package/src/component/Base.mjs +6 -16
  26. package/src/controller/Base.mjs +87 -63
  27. package/src/core/Base.mjs +72 -17
  28. package/src/core/Compare.mjs +3 -13
  29. package/src/core/Config.mjs +139 -0
  30. package/src/core/ConfigSymbols.mjs +3 -0
  31. package/src/core/Util.mjs +3 -18
  32. package/src/data/RecordFactory.mjs +22 -3
  33. package/src/form/field/ComboBox.mjs +6 -1
  34. package/src/util/Function.mjs +52 -5
  35. package/src/vdom/Helper.mjs +7 -5
  36. package/test/siesta/tests/ReactiveConfigs.mjs +112 -0
  37. package/learn/guides/CustomComponents.md +0 -45
  38. package/learn/guides/Forms.md +0 -1
  39. package/learn/guides/Layouts.md +0 -1
  40. /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
  41. /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
  42. /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
  43. /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
  44. /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
  45. /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
  46. /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
  47. /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
  48. /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
  49. /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
  50. /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
  51. /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
  52. /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
@@ -0,0 +1,359 @@
1
+
2
+ Neo.mjs is built upon a robust and consistent class system. Understanding how to extend framework classes is fundamental
3
+ to building custom functionality, whether you're creating new UI components, defining data structures, or implementing
4
+ application logic.
5
+
6
+ This guide covers the universal principles of class extension in Neo.mjs, which apply across all class types, not just
7
+ UI components.
8
+
9
+ ## 1. The `static config` Block: Defining Properties
10
+
11
+ Every Neo.mjs class utilizes a `static config` block. This is where you define the properties that instances of your
12
+ class will possess. These properties can be simple values, objects, or even other Neo.mjs class configurations.
13
+
14
+ ```javascript readonly
15
+ class MyBaseClass extends Neo.core.Base {
16
+ static config = {
17
+ className: 'My.Base.Class', // Unique identifier for the class
18
+ myString : 'Hello',
19
+ myNumber : 123
20
+ }
21
+ }
22
+
23
+ export default Neo.setupClass(MyBaseClass);
24
+ ```
25
+
26
+ Common configs you'll encounter include `className` (a unique string identifier for your class) and `ntype` (a shorthand
27
+ alias for component creation).
28
+
29
+ ## 2. Reactive Configs: The Trailing Underscore (`_`)
30
+
31
+ A cornerstone of Neo.mjs's reactivity is the trailing underscore (`_`) convention for configs defined in `static config`.
32
+ When you append an underscore to a config name (e.g., `myConfig_`), the framework automatically generates a reactive
33
+ getter and setter for it.
34
+
35
+ ```javascript readonly
36
+ class MyReactiveClass extends Neo.core.Base {
37
+ static config = {
38
+ className : 'My.Reactive.Class',
39
+ myReactiveConfig_: 'initial value' // This config is reactive
40
+ }
41
+
42
+ onConstructed() {
43
+ super.onConstructed();
44
+ console.log(this.myReactiveConfig); // Accesses the getter
45
+ this.myReactiveConfig = 'new value'; // Triggers the setter
46
+ }
47
+ }
48
+
49
+ export default Neo.setupClass(MyReactiveClass);
50
+ ```
51
+
52
+ Assigning a new value to a reactive property (e.g., `this.myReactiveProp = 'new value'`) triggers its setter, which in
53
+ turn can invoke lifecycle hooks, enabling automatic updates and side effects. Properties without the underscore are
54
+ static and do not trigger this reactive behavior.
55
+
56
+ ## 3. Configuration Lifecycle Hooks (`beforeSet`, `afterSet`, `beforeGet`)
57
+
58
+ For every reactive config (`myConfig_`), Neo.mjs provides three optional lifecycle hooks that you can implement in your
59
+ class. These methods are automatically called by the framework during the config's lifecycle, offering powerful
60
+ interception points:
61
+
62
+ * **`beforeSetMyConfig(value, oldValue)`**:
63
+ * **Purpose**: Intercepts the value *before* it is set. Ideal for validation, type coercion, or transforming the
64
+ incoming value.
65
+ * **Return Value**: Return the (potentially modified) `value` that should be set.
66
+ Returning `undefined` or `null` will prevent the value from being set.
67
+
68
+ * **`afterSetMyConfig(value, oldValue)`**:
69
+ * **Purpose**: Executed *after* the value has been successfully set. Ideal for triggering side effects, updating
70
+ the UI (e.g., calling `this.update()` for components), or firing events.
71
+ * **Return Value**: None.
72
+
73
+ * **`beforeGetMyConfig(value)`**:
74
+ * **Purpose**: Intercepts the value *before* it is returned by the getter. Useful for lazy initialization,
75
+ computing values on demand, or returning a transformed version of the stored value.
76
+ * **Return Value**: Return the `value` that should be returned by the getter.
77
+
78
+
79
+
80
+ ## 4. Flexible Configuration of Instances: The `beforeSetInstance` Pattern
81
+
82
+ Neo.mjs offers significant flexibility in how you configure properties that expect an instance of a Neo.mjs class
83
+ (e.g., `store`, `layout`, `controller`). This flexibility is powered by the `Neo.util.ClassSystem.beforeSetInstance`
84
+ utility, which intelligently converts various input types into the required instance.
85
+
86
+ This pattern is commonly used within `beforeSet` lifecycle hooks to ensure that by the time a config property is set,
87
+ it always holds a valid Neo.mjs instance.
88
+
89
+ You can typically configure such properties using one of three methods:
90
+
91
+ 1. **A Configuration Object (Plain JavaScript Object):**
92
+ Provide a plain JavaScript object with the desired properties. Neo.mjs will automatically create an instance of the
93
+ expected class (e.g., `Neo.data.Store` for the `store` config) using this object as its configuration. This is ideal
94
+ for inline, simple definitions.
95
+
96
+ ```javascript readonly
97
+ store: { // Neo.mjs will create a Store instance from this config
98
+ model: { fields: [{name: 'id'}, {name: 'name'}] },
99
+ data: [{id: 1, name: 'Item 1'}]
100
+ }
101
+ ```
102
+
103
+ 2. **A Class Reference:**
104
+ Pass a direct reference to the Neo.mjs class. The framework will automatically instantiate this class when the
105
+ component is created.
106
+
107
+ ```javascript readonly
108
+ import MyCustomStore from './MyCustomStore.mjs';
109
+
110
+ // ...
111
+ store: MyCustomStore // Neo.mjs will create an instance of MyCustomStore
112
+ ```
113
+
114
+ 3. **A Pre-created Instance:**
115
+ Provide an already instantiated Neo.mjs object (typically created using`Neo.create()`). This is useful when you need
116
+ to share a single instance across multiple components or manage its lifecycle externally.
117
+
118
+ ```javascript readonly
119
+ const mySharedStore = Neo.create(Neo.data.Store, { /* ... */ });
120
+
121
+ // ...
122
+ store: mySharedStore // Pass an already existing Store instance
123
+ ```
124
+
125
+ This flexibility allows you to choose the most convenient and appropriate configuration style for your specific use case,
126
+ from quick inline setups to robust, reusable class-based architectures.
127
+
128
+ ### Real-World Example: `Neo.grid.Container`'s `store` config
129
+
130
+ A prime example of `beforeSetInstance` in action is the `store` config within `Neo.grid.Container`.
131
+ The `beforeSetStore` hook ensures that the `store` property always holds a valid `Neo.data.Store` instance,
132
+ regardless of how it was initially configured.
133
+
134
+ ```javascript readonly
135
+ import ClassSystemUtil from '../../src/util/ClassSystem.mjs';
136
+ import Store from '../../src/data/Store.mjs';
137
+
138
+ class GridContainer extends Neo.container.Base {
139
+ static config = {
140
+ className: 'Neo.grid.Container',
141
+ store_ : null // The reactive store config
142
+ }
143
+
144
+ /**
145
+ * Triggered before the store config gets changed.
146
+ * @param {Object|Neo.data.Store|null} value
147
+ * @param {Neo.data.Store} oldValue
148
+ * @protected
149
+ */
150
+ beforeSetStore(value, oldValue) {
151
+ if (value) {
152
+ // This ensures that 'value' is always a Neo.data.Store instance.
153
+ // It handles plain objects (creating a new Store), class references,
154
+ // or pre-existing instances.
155
+ value = ClassSystemUtil.beforeSetInstance(value, Store);
156
+ }
157
+ return value;
158
+ }
159
+
160
+ // ... other methods
161
+ }
162
+
163
+ Neo.setupClass(GridContainer);
164
+ ```
165
+
166
+ In this example, `ClassSystemUtil.beforeSetInstance(value, Store)` intelligently processes the `value`:
167
+ * If `value` is a plain JavaScript object, it creates a new `Neo.data.Store` instance using that object as its config.
168
+ * If `value` is a `Neo.data.Store` class reference, it instantiates that class.
169
+ * If `value` is already a `Neo.data.Store` instance, it returns it as is.
170
+
171
+ This pattern is crucial for providing a flexible yet robust API for configuring complex properties.
172
+
173
+ ## 5. The Role of `Neo.setupClass()` and the Global `Neo` Namespace
174
+
175
+ When you define a class in Neo.mjs and pass it to `Neo.setupClass()`, the framework performs several crucial operations.
176
+ One of the most significant is to **enhance the global `Neo` namespace** with a reference to your newly defined class.
177
+
178
+ This means that after `Neo.setupClass(MyClass)` is executed, your class becomes accessible globally via
179
+ `Neo.[your.class.name]`, where `[your.class.name]` corresponds to the `className` config you defined (e.g.,
180
+ `Neo.button.Base`, `Neo.form.field.Text`, or your custom `My.Custom.Class`).
181
+
182
+ **Implications for Class Extension and Usage:**
183
+
184
+ * **Global Accessibility**: You can refer to any framework class (or your own custom classes after they've been set
185
+ up) using their full `Neo` namespace path (e.g., `Neo.button.Base`, `Neo.container.Base`) anywhere in your
186
+ application code, even
187
+ without an explicit ES module import for that specific class.
188
+ * **Convenience vs. Best Practice**: While `extends Neo.button.Base` might technically work without an
189
+ `import Button from '...'`, it is generally **not recommended** for application code. Explicit ES module imports
190
+ (e.g., `import Button from '../button/Base.mjs';`) are preferred because they:
191
+ * **Improve Readability**: Clearly show the dependencies of your module.
192
+ * **Enhance Tooling**: Enable better static analysis, auto-completion, and refactoring support in modern IDEs.
193
+ * **Ensure Consistency**: Promote a consistent and predictable coding style.
194
+ * **Framework Internal Use**: The global `Neo` namespace is heavily utilized internally by the framework itself for
195
+ its class registry, dependency resolution, and dynamic instantiation (e.g., when using `ntype` or `module` configs).
196
+
197
+ Understanding this mechanism clarifies how Neo.mjs manages its class system and provides the underlying flexibility for
198
+ its configuration-driven approach.
199
+
200
+ ## 5. Practical Examples: Models, Stores, and Controllers
201
+
202
+ The principles of class extension apply universally across all Neo.mjs class types.
203
+
204
+ ### Extending `Neo.data.Model`
205
+
206
+ Models define the structure and behavior of individual data records. While reactive configs can be used for class-level
207
+ properties of a Model (e.g., a global setting for all products), properties that vary per record (like `price` or
208
+ `discount`) should be defined as fields within the `fields` array. Neo.mjs provides `convert` and `calculate`
209
+ functions directly on field definitions for per-record logic.
210
+
211
+ ```javascript readonly
212
+ import Model from '../../src/data/Model.mjs';
213
+
214
+ class ProductModel extends Model {
215
+ static config = {
216
+ className: 'App.model.Product',
217
+ fields: [
218
+ {name: 'id', type: 'Number'},
219
+ {name: 'name', type: 'String'},
220
+ {name: 'price', type: 'Number', defaultValue: 0,
221
+ // Use a convert function for field-level validation or transformation
222
+ convert: value => {
223
+ if (typeof value !== 'number' || value < 0) {
224
+ console.warn('Price field must be a non-negative number!');
225
+ return 0;
226
+ }
227
+ return value;
228
+ }
229
+ },
230
+ {name: 'discount', type: 'Number', defaultValue: 0,
231
+ // Use a convert function for field-level validation or transformation
232
+ convert: value => {
233
+ if (typeof value !== 'number' || value < 0 || value > 1) {
234
+ console.warn('Discount field must be a number between 0 and 1!');
235
+ return 0;
236
+ }
237
+ return value;
238
+ }
239
+ },
240
+ {name: 'discountedPrice', type: 'Number',
241
+ // Use a calculate function for derived values based on other fields in the record
242
+ calculate: (data) => {
243
+ // 'data' contains the raw field values of the current record
244
+ return data.price * (1 - data.discount);
245
+ }
246
+ }
247
+ ]
248
+ }
249
+ }
250
+
251
+ Neo.setupClass(ProductModel);
252
+ ```
253
+
254
+ ### Extending `Neo.data.Store`
255
+
256
+ Stores manage collections of data records, often using a defined `Model`.
257
+
258
+ ```javascript readonly
259
+ import Store from '../../src/data/Store.mjs';
260
+ import ProductModel from './ProductModel.mjs'; // Assuming ProductModel is in the same directory
261
+
262
+ class ProductsStore extends Store {
263
+ static config = {
264
+ className: 'App.store.Products',
265
+ model : ProductModel, // Use our custom ProductModel
266
+ autoLoad : true,
267
+ url : '/api/products', // Example API endpoint
268
+ sorters : [{
269
+ property : 'name',
270
+ direction: 'ASC'
271
+ }]
272
+ }
273
+
274
+ // Custom method to filter by price range
275
+ filterByPriceRange(min, max) {
276
+ // The idiomatic way to apply filters is by setting the 'filters' config.
277
+ // This replaces any existing filters.
278
+ this.filters = [{
279
+ property: 'price',
280
+ operator: '>=',
281
+ value : min
282
+ }, {
283
+ property: 'price',
284
+ operator: '<=',
285
+ value : max
286
+ }];
287
+ }
288
+
289
+ // To add filters without replacing existing ones, you would typically
290
+ // read the current filters, add new ones, and then set the filters config.
291
+ // Example (conceptual, not part of the class):
292
+ /*
293
+ addPriceRangeFilter(min, max) {
294
+ const currentFilters = this.filters ? [...this.filters] : [];
295
+ currentFilters.push({
296
+ property: 'price',
297
+ operator: '>=',
298
+ value : min
299
+ }, {
300
+ property: 'price',
301
+ operator: '<=',
302
+ value : max
303
+ });
304
+ this.filters = currentFilters;
305
+ }
306
+ */
307
+ }
308
+
309
+ Neo.setupClass(ProductsStore);
310
+ ```
311
+
312
+ ### Extending `Neo.controller.Component`
313
+
314
+ Controllers encapsulate logic related to components, often handling events or managing state.
315
+
316
+ ```javascript readonly
317
+ import ComponentController from '../../src/controller/Component.mjs';
318
+
319
+ class MyCustomController extends ComponentController {
320
+ static config = {
321
+ className: 'App.controller.MyCustom',
322
+ // A reactive property to manage a piece of controller-specific state
323
+ isActive_: false
324
+ }
325
+
326
+ onConstructed() {
327
+ super.onConstructed();
328
+ console.log('MyCustomController constructed!');
329
+ }
330
+
331
+ afterSetIsActive(value, oldValue) {
332
+ console.log(`Controller active state changed from ${oldValue} to ${value}`);
333
+ // Perform actions based on active state change
334
+ if (value) {
335
+ this.doSomethingActive();
336
+ } else {
337
+ this.doSomethingInactive();
338
+ }
339
+ }
340
+
341
+ doSomethingActive() {
342
+ console.log('Controller is now active!');
343
+ // Example: enable a feature, start a timer
344
+ }
345
+
346
+ doSomethingInactive() {
347
+ console.log('Controller is now inactive!');
348
+ // Example: disable a feature, clear a timer
349
+ }
350
+ }
351
+
352
+ Neo.setupClass(MyCustomController);
353
+ ```
354
+
355
+ ## Conclusion
356
+
357
+ The class extension mechanism, coupled with the reactive config system and `Neo.setupClass()`, forms the backbone of
358
+ development in Neo.mjs. By mastering these principles, you can create highly modular, maintainable, and powerful
359
+ applications that seamlessly integrate with the framework's core.
@@ -0,0 +1,287 @@
1
+ ## Introduction
2
+
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.
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.
11
+
12
+ This guide will walk you through the process.
13
+
14
+ ## Choosing the Right Base Class
15
+
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.
111
+
112
+ ```javascript live-preview
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.
117
+ class MySpecialButton extends Button {
118
+ static config = {
119
+ className: 'Example.view.MySpecialButton',
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();
146
+ }
147
+ }
148
+
149
+ MySpecialButton = Neo.setupClass(MySpecialButton);
150
+
151
+
152
+ // 2. Use the new component in a view.
153
+ class MainView extends Container {
154
+ static config = {
155
+ className: 'Example.view.MainView',
156
+ layout : {ntype: 'vbox', align: 'start'},
157
+ items : [{
158
+ // A standard framework button for comparison
159
+ module : Button,
160
+ iconCls: 'fa fa-home',
161
+ text : 'A framework button'
162
+ }, {
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'}
267
+ }]
268
+ }
269
+ }
270
+
271
+ MainView= Neo.setupClass(MainView);
272
+ ```
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).