neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/MainContainerController.mjs +6 -6
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
- package/learn/guides/datahandling/Grids.md +621 -0
- package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
- package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
- package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
- package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
- package/learn/tree.json +64 -57
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +244 -88
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +35 -3
- package/src/component/Base.mjs +72 -61
- package/src/container/Base.mjs +28 -24
- package/src/controller/Base.mjs +87 -63
- package/src/core/Base.mjs +207 -33
- package/src/core/Compare.mjs +3 -13
- package/src/core/Config.mjs +230 -0
- package/src/core/ConfigSymbols.mjs +3 -0
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/core/Util.mjs +3 -18
- package/src/data/RecordFactory.mjs +22 -3
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/util/Function.mjs +52 -5
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/config/Basic.mjs +149 -0
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
- package/learn/guides/ExtendingNeoClasses.md +0 -331
- /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
- /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
- /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
- /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
- /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
- /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
- /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
- /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
- /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
- /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
- /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
- /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
- /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
- /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
- /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
@@ -195,7 +195,8 @@ nested Container which contains the `world` data prop.
|
|
195
195
|
As a result, the bindings for all 3 Labels contain a combination of data props which live inside different stateProviders.
|
196
196
|
As long as these VMs are inside the parent hierarchy this works fine.
|
197
197
|
|
198
|
-
The same goes for the Button handlers: `setData()` will find the closest matching data prop inside the stateProvider
|
198
|
+
The same goes for the Button handlers: `setData()` will find the closest matching data prop inside the stateProvider
|
199
|
+
parent chain.
|
199
200
|
|
200
201
|
We can even change data props which live inside different stateProviders at once. As easy as this:</br>
|
201
202
|
`setData({hello: 'foo', world: 'bar'})`
|
@@ -320,6 +321,7 @@ class MainContainerController extends Controller {
|
|
320
321
|
animateTargetId: me.getReference('edit-user-button').id,
|
321
322
|
appName : me.component.appName,
|
322
323
|
closeAction : 'hide',
|
324
|
+
modal : true,
|
323
325
|
|
324
326
|
stateProvider: {
|
325
327
|
parent: me.getStateProvider()
|
@@ -437,3 +439,146 @@ class MainView extends Container {
|
|
437
439
|
}
|
438
440
|
MainView = Neo.setupClass(MainView);
|
439
441
|
```
|
442
|
+
|
443
|
+
### Managing Stores with State Providers
|
444
|
+
|
445
|
+
Beyond managing simple data properties, `Neo.state.Provider` can also centralize the management of `Neo.data.Store`
|
446
|
+
instances. This is particularly useful for sharing data across multiple components or for complex data flows within
|
447
|
+
your application.
|
448
|
+
|
449
|
+
You define stores within the `stores` config of your `StateProvider` class. Each entry
|
450
|
+
in the `stores` object can either be an inline store configuration (a plain JavaScript
|
451
|
+
object) or a class reference to a `Neo.data.Store` subclass.
|
452
|
+
|
453
|
+
It is also a common practice to import a `Neo.data.Model` extension and use it within
|
454
|
+
an inline store configuration, like so:
|
455
|
+
|
456
|
+
```javascript readonly
|
457
|
+
import MyCustomModel from './MyCustomModel.mjs'; // Assuming MyCustomModel extends Neo.data.Model
|
458
|
+
|
459
|
+
// ...
|
460
|
+
stores: {
|
461
|
+
myStore: {
|
462
|
+
model: MyCustomModel,
|
463
|
+
// other inline configs like autoLoad, data, url
|
464
|
+
}
|
465
|
+
}
|
466
|
+
```
|
467
|
+
|
468
|
+
Components can then bind to these centrally managed stores using the `bind` config,
|
469
|
+
referencing the store by its key within the `stores` object (e.g., `stores.myStoreName`).
|
470
|
+
|
471
|
+
```javascript live-preview
|
472
|
+
import Button from '../button/Base.mjs';
|
473
|
+
import Container from '../container/Base.mjs';
|
474
|
+
import GridContainer from '../grid/Container.mjs';
|
475
|
+
import Label from '../component/Label.mjs';
|
476
|
+
import StateProvider from '../state/Provider.mjs';
|
477
|
+
import Store from '../data/Store.mjs';
|
478
|
+
|
479
|
+
class MyDataStore extends Store {
|
480
|
+
static config = {
|
481
|
+
className: 'Guides.vm7.MyDataStore',
|
482
|
+
model: {
|
483
|
+
fields: [
|
484
|
+
{name: 'id', type: 'Number'},
|
485
|
+
{name: 'name', type: 'String'}
|
486
|
+
]
|
487
|
+
},
|
488
|
+
data: [
|
489
|
+
{id: 1, name: 'Item A'},
|
490
|
+
{id: 2, name: 'Item B'},
|
491
|
+
{id: 3, name: 'Item C'}
|
492
|
+
]
|
493
|
+
}
|
494
|
+
}
|
495
|
+
MyDataStore = Neo.setupClass(MyDataStore);
|
496
|
+
|
497
|
+
class MainViewStateProvider extends StateProvider {
|
498
|
+
static config = {
|
499
|
+
className: 'Guides.vm7.MainViewStateProvider',
|
500
|
+
|
501
|
+
data: {
|
502
|
+
myStoreCount: 0
|
503
|
+
},
|
504
|
+
|
505
|
+
stores: {
|
506
|
+
// Define a store using a class reference
|
507
|
+
mySharedStore: {
|
508
|
+
module : MyDataStore,
|
509
|
+
listeners: {countChange: 'onMyStoreCountChange'}
|
510
|
+
},
|
511
|
+
// Define another store using an inline configuration
|
512
|
+
anotherStore: {
|
513
|
+
module: Store,
|
514
|
+
model: {
|
515
|
+
fields: [
|
516
|
+
{name: 'value', type: 'Number'}
|
517
|
+
]
|
518
|
+
},
|
519
|
+
data: [
|
520
|
+
{value: 10},
|
521
|
+
{value: 20},
|
522
|
+
{value: 30}
|
523
|
+
]
|
524
|
+
}
|
525
|
+
}
|
526
|
+
}
|
527
|
+
|
528
|
+
onMyStoreCountChange(data) {
|
529
|
+
this.data.myStoreCount = data.value // Reactive
|
530
|
+
}
|
531
|
+
}
|
532
|
+
MainViewStateProvider = Neo.setupClass(MainViewStateProvider);
|
533
|
+
|
534
|
+
class MainView extends Container {
|
535
|
+
static config = {
|
536
|
+
className : 'Guides.vm7.MainView',
|
537
|
+
stateProvider: MainViewStateProvider, // Assign the state provider
|
538
|
+
width : 300,
|
539
|
+
|
540
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
541
|
+
items: [{
|
542
|
+
module: GridContainer,
|
543
|
+
flex : 1,
|
544
|
+
bind: {
|
545
|
+
// Bind the grid's store config to 'mySharedStore'
|
546
|
+
store: 'stores.mySharedStore'
|
547
|
+
},
|
548
|
+
columns: [
|
549
|
+
{text: 'Id', dataField: 'id'},
|
550
|
+
{text: 'Name', dataField: 'name', flex: 1}
|
551
|
+
]
|
552
|
+
}, {
|
553
|
+
module: Container,
|
554
|
+
flex : 'none',
|
555
|
+
layout: {ntype: 'hbox', align: 'stretch'},
|
556
|
+
items: [{
|
557
|
+
module: Label,
|
558
|
+
style : {margin: 'auto'},
|
559
|
+
bind: {
|
560
|
+
text: data => `Count: ${data.myStoreCount}`
|
561
|
+
}
|
562
|
+
}, {
|
563
|
+
module: Button,
|
564
|
+
text : 'Add Item to Store',
|
565
|
+
handler() {
|
566
|
+
const store = this.getStateProvider().getStore('mySharedStore');
|
567
|
+
store.add({id: store.getCount() + 1, name: 'New Item'})
|
568
|
+
}
|
569
|
+
}]
|
570
|
+
}]
|
571
|
+
}
|
572
|
+
}
|
573
|
+
MainView = Neo.setupClass(MainView);
|
574
|
+
```
|
575
|
+
|
576
|
+
In this example:
|
577
|
+
* `MainViewStateProvider` defines two stores: `mySharedStore` (using a class reference) and
|
578
|
+
`anotherStore` (using an inline config).
|
579
|
+
* A `GridContainer` binds its `store` config directly to `mySharedStore`, allowing it to
|
580
|
+
display and interact with the data.
|
581
|
+
* A `Button` demonstrates how to programmatically interact with the store by adding a new record.
|
582
|
+
|
583
|
+
This approach provides a clean and efficient way to manage and share data across your
|
584
|
+
application, leveraging the power of the state provider system.
|
@@ -0,0 +1,166 @@
|
|
1
|
+
### A New Approach: Declarative VDOM with Effects
|
2
|
+
|
3
|
+
Neo.mjs v10 introduces a powerful new declarative pattern for defining a component's internal Virtual DOM (VDOM), serving as an alternative to the traditional imperative hook-based system. This approach, which leverages the new `Neo.core.Effect` class, allows developers to define a component's entire VDOM structure in a single, reactive function, similar to the `render()` method in React.
|
4
|
+
|
5
|
+
This guide will walk you through the new pattern, compare it to the classic approach, and explain when to use each.
|
6
|
+
|
7
|
+
### The Classic Pattern: Imperative Hooks
|
8
|
+
|
9
|
+
Let's look at the traditional way a component like `Neo.button.Base` defines and updates its VDOM.
|
10
|
+
|
11
|
+
**1. Initial VDOM Structure:**
|
12
|
+
The base structure is defined in the `_vdom` config.
|
13
|
+
|
14
|
+
```javascript readonly
|
15
|
+
// Neo.button.Base
|
16
|
+
class Button extends Component {
|
17
|
+
static config = {
|
18
|
+
_vdom: {
|
19
|
+
tag: 'button', type: 'button', cn: [
|
20
|
+
{tag: 'span', cls: ['neo-button-glyph']},
|
21
|
+
{tag: 'span', cls: ['neo-button-text']},
|
22
|
+
// ... and so on
|
23
|
+
]
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
```
|
28
|
+
|
29
|
+
**2. Imperative Updates:**
|
30
|
+
To make the component reactive, developers must implement specific `afterSet` hooks for each config that affects the UI. The logic is imperative and fragmented.
|
31
|
+
|
32
|
+
```javascript readonly
|
33
|
+
// Neo.button.Base - internal framework code
|
34
|
+
afterSetIconCls(value, oldValue) {
|
35
|
+
let {iconNode} = this;
|
36
|
+
// Imperative: Manually add/remove classes
|
37
|
+
NeoArray.remove(iconNode.cls, oldValue);
|
38
|
+
NeoArray.add(iconNode.cls, value);
|
39
|
+
this.update();
|
40
|
+
}
|
41
|
+
|
42
|
+
afterSetText(value, oldValue) {
|
43
|
+
let {textNode} = this;
|
44
|
+
// Imperative: Manually set properties
|
45
|
+
textNode.removeDom = !value;
|
46
|
+
textNode.text = value;
|
47
|
+
this.update();
|
48
|
+
}
|
49
|
+
|
50
|
+
afterSetPressed(value, oldValue) {
|
51
|
+
// Imperative: Manually toggle a class
|
52
|
+
NeoArray.toggle(this.cls, 'pressed', value);
|
53
|
+
this.update();
|
54
|
+
}
|
55
|
+
```
|
56
|
+
|
57
|
+
**Pros:**
|
58
|
+
* **Performance:** Updates are surgical and extremely fast. Only the code for the changed property is executed.
|
59
|
+
|
60
|
+
**Cons:**
|
61
|
+
* **High Cognitive Load:** To understand the component's full rendering logic, a developer must find and read multiple, separate methods.
|
62
|
+
* **Error-Prone:** Forgetting to implement a hook for a new config is a common source of bugs.
|
63
|
+
|
64
|
+
### The New Pattern: Declarative VDOM with `Effect`
|
65
|
+
|
66
|
+
The new `EffectButton` PoC demonstrates a more modern, declarative approach.
|
67
|
+
|
68
|
+
**1. A Single, Reactive Render Function:**
|
69
|
+
Instead of fragmented hooks, the entire VDOM is generated within a `Neo.core.Effect`. This effect automatically tracks its dependencies (like `this.text` or `this.pressed`) and re-runs whenever they change.
|
70
|
+
|
71
|
+
```javascript readonly
|
72
|
+
// button.Effect - The "Template Method"
|
73
|
+
createVdomEffect() {
|
74
|
+
return new Effect({fn: () => {
|
75
|
+
// The effect's only job is to get the config and trigger an update.
|
76
|
+
this._vdom = this.getVdomConfig();
|
77
|
+
this.update();
|
78
|
+
}});
|
79
|
+
}
|
80
|
+
|
81
|
+
// The main VDOM builder
|
82
|
+
getVdomConfig() {
|
83
|
+
return {
|
84
|
+
tag: this.pressed ? 'a' : 'button', // Declarative logic
|
85
|
+
cls: this.getVdomCls(),
|
86
|
+
cn: this.getVdomChildren()
|
87
|
+
// ... and so on
|
88
|
+
};
|
89
|
+
}
|
90
|
+
```
|
91
|
+
|
92
|
+
**2. Centralized Logic:**
|
93
|
+
All VDOM logic is co-located, making it easy to read and understand at a glance.
|
94
|
+
|
95
|
+
```javascript readonly
|
96
|
+
// button.Effect - Centralized class and child generation
|
97
|
+
getVdomCls() {
|
98
|
+
let vdomCls = [...this.baseCls, ...this.cls];
|
99
|
+
// Declarative: Describe what the classes should be based on state
|
100
|
+
NeoArray.toggle(vdomCls, 'no-text', !this.text);
|
101
|
+
NeoArray.toggle(vdomCls, 'pressed', this.pressed);
|
102
|
+
vdomCls.push('icon-' + this.iconPosition);
|
103
|
+
return vdomCls;
|
104
|
+
}
|
105
|
+
|
106
|
+
getVdomChildren() {
|
107
|
+
return [
|
108
|
+
// Declarative: Describe the children based on state
|
109
|
+
{tag: 'span', cls: ['neo-button-glyph', ...this._iconCls || []], removeDom: !this.iconCls},
|
110
|
+
{tag: 'span', cls: ['neo-button-text'], removeDom: !this.text, text: this.text},
|
111
|
+
// ... and so on
|
112
|
+
];
|
113
|
+
}
|
114
|
+
```
|
115
|
+
|
116
|
+
### The Power of Inheritance
|
117
|
+
|
118
|
+
A key challenge with a single render function is extensibility. The new pattern solves this by using a "Template Method" design. The main effect calls smaller, overridable builder methods.
|
119
|
+
|
120
|
+
This allows a subclass like `tab.header.EffectButton` to easily extend the VDOM without duplicating code.
|
121
|
+
|
122
|
+
```javascript readonly
|
123
|
+
// tab.header.EffectButton
|
124
|
+
class EffectTabButton extends EffectButton {
|
125
|
+
// Override to add the indicator child node
|
126
|
+
getVdomChildren() {
|
127
|
+
// Get the standard button children from the parent class
|
128
|
+
let children = super.getVdomChildren();
|
129
|
+
|
130
|
+
// Add the new indicator node
|
131
|
+
children.push({
|
132
|
+
cls: ['neo-tab-button-indicator'],
|
133
|
+
removeDom: !this.useActiveTabIndicator
|
134
|
+
});
|
135
|
+
|
136
|
+
return children;
|
137
|
+
}
|
138
|
+
|
139
|
+
// Override to add accessibility attributes
|
140
|
+
getVdomConfig() {
|
141
|
+
let vdomConfig = super.getVdomConfig();
|
142
|
+
vdomConfig.role = this.role;
|
143
|
+
if (this.pressed) {
|
144
|
+
vdomConfig['aria-selected'] = true;
|
145
|
+
}
|
146
|
+
return vdomConfig;
|
147
|
+
}
|
148
|
+
}
|
149
|
+
```
|
150
|
+
|
151
|
+
### When to Use Each Pattern: A Hybrid Approach
|
152
|
+
|
153
|
+
Neo.mjs v10 does not force you to choose one pattern over the other. Instead, it empowers you to use the right tool for the job.
|
154
|
+
|
155
|
+
**Use the Declarative `Effect` Pattern when (Recommended Default):**
|
156
|
+
* Building most of your application components.
|
157
|
+
* You value developer experience, readability, and maintainability.
|
158
|
+
* The component's VDOM structure can be expressed as a pure function of its state.
|
159
|
+
|
160
|
+
**Use the Imperative `afterSet` Pattern when:**
|
161
|
+
* You are building a highly complex, performance-critical component (e.g., a virtualized data grid or a canvas-based chart).
|
162
|
+
* You need to perform surgical, hand-tuned VDOM manipulations for maximum performance, bypassing a full recalculation.
|
163
|
+
|
164
|
+
### Conclusion
|
165
|
+
|
166
|
+
The new declarative VDOM pattern is a major leap forward for component development in Neo.mjs. It provides a more modern, readable, and robust way to build components, while the classic imperative pattern remains a powerful tool for fine-grained performance optimization. By understanding both, you can build sophisticated, high-performance applications with an exceptional developer experience.
|
@@ -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.
|