neo.mjs 10.0.0-beta.3 → 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.
- 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/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} +145 -1
- 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 +63 -57
- package/package.json +2 -2
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +37 -29
- package/src/collection/Base.mjs +29 -2
- package/src/component/Base.mjs +6 -16
- package/src/controller/Base.mjs +87 -63
- package/src/core/Base.mjs +72 -17
- package/src/core/Compare.mjs +3 -13
- package/src/core/Config.mjs +139 -0
- package/src/core/ConfigSymbols.mjs +3 -0
- package/src/core/Util.mjs +3 -18
- package/src/data/RecordFactory.mjs +22 -3
- package/src/util/Function.mjs +52 -5
- package/test/siesta/tests/ReactiveConfigs.mjs +112 -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
package/src/util/Function.mjs
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
const originalMethodSymbol = Symbol('originalMethod');
|
2
|
+
const sequencedFnsSymbol = Symbol('sequencedFns');
|
3
|
+
|
1
4
|
/**
|
2
5
|
* Append args instead of prepending them
|
3
6
|
* @param {Function} fn
|
@@ -67,12 +70,30 @@ export function createInterceptor(target, targetMethodName, interceptFunction, s
|
|
67
70
|
* @returns {Function}
|
68
71
|
*/
|
69
72
|
export function createSequence(target, methodName, fn, scope) {
|
70
|
-
let
|
73
|
+
let currentMethod = target[methodName],
|
74
|
+
wrapper;
|
75
|
+
|
76
|
+
if (currentMethod && currentMethod[sequencedFnsSymbol]) {
|
77
|
+
// Already a sequenced method, add to its list
|
78
|
+
wrapper = currentMethod;
|
79
|
+
wrapper[sequencedFnsSymbol].push({fn, scope})
|
80
|
+
} else {
|
81
|
+
// First time sequencing this method
|
82
|
+
let originalMethod = currentMethod || Neo.emptyFn;
|
83
|
+
|
84
|
+
wrapper = function() {
|
85
|
+
originalMethod.apply(this, arguments); // Call the original method
|
86
|
+
|
87
|
+
// Call all sequenced functions
|
88
|
+
wrapper[sequencedFnsSymbol].forEach(seqFn => {
|
89
|
+
seqFn.fn.apply(seqFn.scope || this, arguments);
|
90
|
+
});
|
91
|
+
};
|
92
|
+
wrapper[sequencedFnsSymbol] = [{fn, scope}];
|
93
|
+
wrapper[originalMethodSymbol] = originalMethod; // Store original method
|
94
|
+
}
|
71
95
|
|
72
|
-
return (target[methodName] =
|
73
|
-
method.apply(this, arguments);
|
74
|
-
return fn.apply(scope || this, arguments)
|
75
|
-
})
|
96
|
+
return (target[methodName] = wrapper);
|
76
97
|
}
|
77
98
|
|
78
99
|
/**
|
@@ -178,3 +199,29 @@ export function throttle(callback, scope, delay=300) {
|
|
178
199
|
}
|
179
200
|
}
|
180
201
|
}
|
202
|
+
|
203
|
+
/**
|
204
|
+
* @param {Neo.core.Base} target
|
205
|
+
* @param {String} methodName
|
206
|
+
* @param {Function} fn
|
207
|
+
* @param {Object} scope
|
208
|
+
*/
|
209
|
+
export function unSequence(target, methodName, fn, scope) {
|
210
|
+
let currentMethod = target[methodName];
|
211
|
+
|
212
|
+
if (!currentMethod || !currentMethod[sequencedFnsSymbol]) {
|
213
|
+
return // Not a sequenced method
|
214
|
+
}
|
215
|
+
|
216
|
+
const sequencedFunctions = currentMethod[sequencedFnsSymbol];
|
217
|
+
|
218
|
+
// Filter out the function to unsequence
|
219
|
+
currentMethod[sequencedFnsSymbol] = sequencedFunctions.filter(seqFn =>
|
220
|
+
!(seqFn.fn === fn && seqFn.scope === scope)
|
221
|
+
);
|
222
|
+
|
223
|
+
if (currentMethod[sequencedFnsSymbol].length === 0) {
|
224
|
+
// If no functions left, restore the original method
|
225
|
+
target[methodName] = currentMethod[originalMethodSymbol]
|
226
|
+
}
|
227
|
+
}
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import Neo from '../../../src/Neo.mjs';
|
2
|
+
import * as core from '../../../src/core/_export.mjs';
|
3
|
+
import {isDescriptor} from '../../../src/core/ConfigSymbols.mjs';
|
4
|
+
|
5
|
+
class MyComponent extends core.Base {
|
6
|
+
static config = {
|
7
|
+
className: 'Neo.TestComponent',
|
8
|
+
myConfig_ : 'initialValue',
|
9
|
+
arrayConfig_: {
|
10
|
+
[isDescriptor]: true,
|
11
|
+
value: [],
|
12
|
+
merge: 'replace'
|
13
|
+
},
|
14
|
+
objectConfig_: {
|
15
|
+
[isDescriptor]: true,
|
16
|
+
value: {},
|
17
|
+
merge: 'deep'
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
afterSetMyConfig(value, oldValue) {
|
22
|
+
// This will be called by the framework
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
MyComponent = Neo.setupClass(MyComponent);
|
27
|
+
|
28
|
+
StartTest(t => {
|
29
|
+
t.it('Basic reactivity with subscribe', t => {
|
30
|
+
const instance = Neo.create(MyComponent);
|
31
|
+
const configController = instance.getConfig('myConfig');
|
32
|
+
|
33
|
+
let subscriberCalled = false;
|
34
|
+
let receivedNewValue, receivedOldValue;
|
35
|
+
|
36
|
+
const cleanup = configController.subscribe((newValue, oldValue) => {
|
37
|
+
subscriberCalled = true;
|
38
|
+
receivedNewValue = newValue;
|
39
|
+
receivedOldValue = oldValue;
|
40
|
+
});
|
41
|
+
|
42
|
+
instance.myConfig = 'newValue';
|
43
|
+
|
44
|
+
t.ok(subscriberCalled, 'Subscriber callback should be called');
|
45
|
+
t.is(receivedNewValue, 'newValue', 'New value should be passed to subscriber');
|
46
|
+
t.is(receivedOldValue, 'initialValue', 'Old value should be passed to subscriber');
|
47
|
+
|
48
|
+
// Test cleanup
|
49
|
+
subscriberCalled = false;
|
50
|
+
cleanup();
|
51
|
+
instance.myConfig = 'anotherValue';
|
52
|
+
t.notOk(subscriberCalled, 'Subscriber callback should not be called after cleanup');
|
53
|
+
});
|
54
|
+
|
55
|
+
t.it('Descriptor: arrayConfig_ with merge: replace', t => {
|
56
|
+
const instance = Neo.create(MyComponent);
|
57
|
+
const configController = instance.getConfig('arrayConfig');
|
58
|
+
|
59
|
+
let subscriberCalled = 0;
|
60
|
+
configController.subscribe((newValue, oldValue) => {
|
61
|
+
subscriberCalled++;
|
62
|
+
});
|
63
|
+
|
64
|
+
const arr1 = [1, 2, 3];
|
65
|
+
instance.arrayConfig = arr1;
|
66
|
+
t.is(instance.arrayConfig, arr1, 'Array should be replaced');
|
67
|
+
t.is(subscriberCalled, 1, 'Subscriber called once for array replacement');
|
68
|
+
|
69
|
+
const arr2 = [4, 5, 6];
|
70
|
+
instance.arrayConfig = arr2;
|
71
|
+
t.is(instance.arrayConfig, arr2, 'Array should be replaced again');
|
72
|
+
t.is(subscriberCalled, 2, 'Subscriber called twice for array replacement');
|
73
|
+
|
74
|
+
// Setting the same array should not trigger a change by default isEqual
|
75
|
+
instance.arrayConfig = arr2;
|
76
|
+
t.is(subscriberCalled, 2, 'Subscriber not called when setting the same array reference');
|
77
|
+
});
|
78
|
+
|
79
|
+
t.it('Descriptor: objectConfig_ with merge: deep', t => {
|
80
|
+
const instance = Neo.create(MyComponent);
|
81
|
+
const configController = instance.getConfig('objectConfig');
|
82
|
+
|
83
|
+
let subscriberCalled = 0;
|
84
|
+
configController.subscribe((newValue, oldValue) => {
|
85
|
+
subscriberCalled++;
|
86
|
+
});
|
87
|
+
|
88
|
+
const obj1 = {a: 1, b: {c: 2}};
|
89
|
+
instance.objectConfig = obj1;
|
90
|
+
t.is(instance.objectConfig, obj1, 'Object should be set');
|
91
|
+
t.is(subscriberCalled, 1, 'Subscriber called once for object set');
|
92
|
+
|
93
|
+
// Deep merge should happen, but default isEqual will still compare references
|
94
|
+
const obj2 = {a: 1, b: {c: 3}};
|
95
|
+
instance.objectConfig = obj2;
|
96
|
+
t.is(instance.objectConfig.a, 1, 'Object property a should be 1');
|
97
|
+
t.is(instance.objectConfig.b.c, 3, 'Object property b.c should be 3');
|
98
|
+
t.is(subscriberCalled, 2, 'Subscriber called twice for object change');
|
99
|
+
|
100
|
+
// Setting the same object reference should not trigger a change
|
101
|
+
instance.objectConfig = obj2;
|
102
|
+
t.is(subscriberCalled, 2, 'Subscriber not called when setting the same object reference');
|
103
|
+
|
104
|
+
// Modifying a nested property should trigger a change if isEqual is deep
|
105
|
+
// NOTE: The current Config.mjs uses Neo.isEqual which is a deep comparison.
|
106
|
+
// If the object reference changes, it will trigger. If the object reference stays the same, but content changes, it will not trigger unless isEqual is customized.
|
107
|
+
// For now, this test relies on the fact that setting a new object reference triggers the change.
|
108
|
+
const obj3 = {a: 1, b: {c: 2}};
|
109
|
+
instance.objectConfig = obj3;
|
110
|
+
t.is(subscriberCalled, 3, 'Subscriber called for new object reference');
|
111
|
+
});
|
112
|
+
});
|
@@ -1,331 +0,0 @@
|
|
1
|
-
# Extending Neo Classes
|
2
|
-
|
3
|
-
Neo.mjs is built upon a robust and consistent class system. Understanding how to extend framework classes is fundamental to building custom functionality, whether you're creating new UI components, defining data structures, or implementing application logic.
|
4
|
-
|
5
|
-
This guide covers the universal principles of class extension in Neo.mjs, which apply across all class types, not just UI components.
|
6
|
-
|
7
|
-
## 1. The `static config` Block: Defining Properties
|
8
|
-
|
9
|
-
Every Neo.mjs class utilizes a `static config` block. This is where you define the properties that instances of your class will possess. These properties can be simple values, objects, or even other Neo.mjs class configurations.
|
10
|
-
|
11
|
-
```javascript readonly
|
12
|
-
class MyBaseClass extends Neo.core.Base {
|
13
|
-
static config = {
|
14
|
-
className: 'My.Base.Class', // Unique identifier for the class
|
15
|
-
myString : 'Hello',
|
16
|
-
myNumber : 123
|
17
|
-
}
|
18
|
-
}
|
19
|
-
|
20
|
-
export default Neo.setupClass(MyBaseClass);
|
21
|
-
```
|
22
|
-
|
23
|
-
Common configs you'll encounter include `className` (a unique string identifier for your class) and `ntype` (a shorthand alias for component creation).
|
24
|
-
|
25
|
-
## 2. Reactive Configs: The Trailing Underscore (`_`)
|
26
|
-
|
27
|
-
A cornerstone of Neo.mjs's reactivity is the trailing underscore (`_`) convention for configs defined in `static config`. When you append an underscore to a config name (e.g., `myConfig_`), the framework automatically generates a reactive getter and setter for it.
|
28
|
-
|
29
|
-
```javascript readonly
|
30
|
-
class MyReactiveClass extends Neo.core.Base {
|
31
|
-
static config = {
|
32
|
-
className : 'My.Reactive.Class',
|
33
|
-
myReactiveConfig_: 'initial value' // This config is reactive
|
34
|
-
}
|
35
|
-
|
36
|
-
onConstructed() {
|
37
|
-
super.onConstructed();
|
38
|
-
console.log(this.myReactiveConfig); // Accesses the getter
|
39
|
-
this.myReactiveConfig = 'new value'; // Triggers the setter
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
export default Neo.setupClass(MyReactiveClass);
|
44
|
-
```
|
45
|
-
|
46
|
-
Assigning a new value to a reactive property (e.g., `this.myReactiveProp = 'new value'`) triggers its setter, which in turn can invoke lifecycle hooks, enabling automatic updates and side effects. Properties without the underscore are static and do not trigger this reactive behavior.
|
47
|
-
|
48
|
-
## 3. Configuration Lifecycle Hooks (`beforeSet`, `afterSet`, `beforeGet`)
|
49
|
-
|
50
|
-
For every reactive config (`myConfig_`), Neo.mjs provides three optional lifecycle hooks that you can implement in your class. These methods are automatically called by the framework during the config's lifecycle, offering powerful interception points:
|
51
|
-
|
52
|
-
* **`beforeSetMyConfig(value, oldValue)`**:
|
53
|
-
* **Purpose**: Intercepts the value *before* it is set. Ideal for validation, type coercion, or transforming the incoming value.
|
54
|
-
* **Return Value**: Return the (potentially modified) `value` that should be set. Returning `undefined` or `null` will prevent the value from being set.
|
55
|
-
|
56
|
-
* **`afterSetMyConfig(value, oldValue)`**:
|
57
|
-
* **Purpose**: Executed *after* the value has been successfully set. Ideal for triggering side effects, updating the UI (e.g., calling `this.update()` for components), or firing events.
|
58
|
-
* **Return Value**: None.
|
59
|
-
|
60
|
-
* **`beforeGetMyConfig(value)`**:
|
61
|
-
* **Purpose**: Intercepts the value *before* it is returned by the getter. Useful for lazy initialization, computing values on demand, or returning a transformed version of the stored value.
|
62
|
-
* **Return Value**: Return the `value` that should be returned by the getter.
|
63
|
-
|
64
|
-
### Overriding Lifecycle Hooks: `super` vs. Full Override
|
65
|
-
|
66
|
-
When extending a Neo.mjs class, you often need to customize the behavior of inherited lifecycle hooks (like `afterSet*`, `onConstructed`, etc.). You have two primary approaches:
|
67
|
-
|
68
|
-
#### 1. Extending Parent Behavior (Calling `super`)
|
69
|
-
|
70
|
-
This is the most common and recommended approach. By calling `super.methodName(...)`, you ensure that the parent class's implementation of the hook is executed. You can then add your custom logic either before or after the `super` call.
|
71
|
-
|
72
|
-
This approach is crucial for maintaining the framework's intended behavior and ensuring that inherited features continue to function correctly.
|
73
|
-
|
74
|
-
```javascript readonly
|
75
|
-
import Button from '../../src/button/Base.mjs';
|
76
|
-
|
77
|
-
class MyExtendedButton extends Button {
|
78
|
-
static config = {
|
79
|
-
className: 'My.Extended.Component',
|
80
|
-
// text_ config is inherited from Button.Base
|
81
|
-
// We can set a default value here if needed, or rely on button.Base's default
|
82
|
-
text: 'New Default Text'
|
83
|
-
}
|
84
|
-
|
85
|
-
// Example: Adding logic after the parent's afterSetText
|
86
|
-
afterSetText(value, oldValue) {
|
87
|
-
// Add your custom pre-processing logic here
|
88
|
-
super.afterSetText(value, oldValue);
|
89
|
-
console.log(`Custom logic: Button text changed to "${value}"`);
|
90
|
-
// Add your custom post-processing logic here
|
91
|
-
}
|
92
|
-
}
|
93
|
-
|
94
|
-
export default Neo.setupClass(MyExtendedButton);
|
95
|
-
```
|
96
|
-
|
97
|
-
#### 2. Completely Overriding Parent Behavior (No `super` Call)
|
98
|
-
|
99
|
-
In rare cases, you might want to completely replace the parent class's implementation of a hook. This is achieved by simply omitting the `super` call within your overridden method.
|
100
|
-
|
101
|
-
**Caution**: Use this approach with extreme care. You must fully understand the parent's implementation and ensure that your override does not break essential framework functionality or inherited features. This is generally reserved for advanced scenarios where you need full control over the hook's execution.
|
102
|
-
|
103
|
-
```javascript readonly
|
104
|
-
import Button from '../../src/button/Base.mjs';
|
105
|
-
|
106
|
-
class MyFullyOverriddenButton extends Button {
|
107
|
-
static config = {
|
108
|
-
className: 'My.Fully.Overridden.Component',
|
109
|
-
text : 'New Default Text'
|
110
|
-
}
|
111
|
-
|
112
|
-
// Example: Completely overriding afterSetText
|
113
|
-
afterSetText(value, oldValue) {
|
114
|
-
// No super.afterSetText(value, oldValue); call
|
115
|
-
console.log(`Fully custom logic: Button text changed to "${value}"`);
|
116
|
-
// The parent's afterSetText will NOT be executed
|
117
|
-
// This means that in this case you need to take care on your own to map the text value to the vdom.
|
118
|
-
}
|
119
|
-
}
|
120
|
-
|
121
|
-
export default Neo.setupClass(MyFullyOverriddenButton);
|
122
|
-
```
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
```javascript readonly
|
127
|
-
class MyHookedClass extends Neo.core.Base {
|
128
|
-
static config = {
|
129
|
-
className: 'My.Hooked.Class',
|
130
|
-
myValue_ : 0
|
131
|
-
}
|
132
|
-
|
133
|
-
beforeSetMyValue(value, oldValue) {
|
134
|
-
if (typeof value !== 'number' || value < 0) {
|
135
|
-
console.warn('myValue must be a non-negative number!');
|
136
|
-
return 0; // Default to 0 if invalid
|
137
|
-
}
|
138
|
-
return value;
|
139
|
-
}
|
140
|
-
|
141
|
-
afterSetMyValue(value, oldValue) {
|
142
|
-
console.log(`myValue changed from ${oldValue} to ${value}`);
|
143
|
-
// In a component, you might call this.update() here
|
144
|
-
}
|
145
|
-
|
146
|
-
beforeGetMyValue(value) {
|
147
|
-
// Example: lazy initialization or computed value
|
148
|
-
if (value === 0 && !this._initialized) {
|
149
|
-
console.log('Initializing myValue on first access');
|
150
|
-
this._initialized = true;
|
151
|
-
return 10; // Return a default initial value
|
152
|
-
}
|
153
|
-
return value;
|
154
|
-
}
|
155
|
-
}
|
156
|
-
|
157
|
-
export default Neo.setupClass(MyHookedClass);
|
158
|
-
```
|
159
|
-
|
160
|
-
## 4. The Role of `Neo.setupClass()` and the Global `Neo` Namespace
|
161
|
-
|
162
|
-
When you define a class in Neo.mjs and pass it to `Neo.setupClass()`, the framework performs several crucial operations. One of the most significant is to **enhance the global `Neo` namespace** with a reference to your newly defined class.
|
163
|
-
|
164
|
-
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`, or your custom `My.Custom.Class`).
|
165
|
-
|
166
|
-
**Implications for Class Extension and Usage:**
|
167
|
-
|
168
|
-
* **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.
|
169
|
-
* **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:
|
170
|
-
* **Improve Readability**: Clearly show the dependencies of your module.
|
171
|
-
* **Enhance Tooling**: Enable better static analysis, auto-completion, and refactoring support in modern IDEs.
|
172
|
-
* **Ensure Consistency**: Promote a consistent and predictable coding style.
|
173
|
-
* **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).
|
174
|
-
|
175
|
-
Understanding this mechanism clarifies how Neo.mjs manages its class system and provides the underlying flexibility for its configuration-driven approach.
|
176
|
-
|
177
|
-
## 5. Practical Examples: Models, Stores, and Controllers
|
178
|
-
|
179
|
-
The principles of class extension apply universally across all Neo.mjs class types.
|
180
|
-
|
181
|
-
### Extending `Neo.data.Model`
|
182
|
-
|
183
|
-
Models define the structure and behavior of individual data records. While reactive configs can be used for class-level properties of a Model (e.g., a global setting for all products), properties that vary per record (like `price` or `discount`) should be defined as fields within the `fields` array. Neo.mjs provides `convert` and `calculate` functions directly on field definitions for per-record logic.
|
184
|
-
|
185
|
-
```javascript readonly
|
186
|
-
import Model from '../../src/data/Model.mjs';
|
187
|
-
|
188
|
-
class ProductModel extends Model {
|
189
|
-
static config = {
|
190
|
-
className: 'App.model.Product',
|
191
|
-
fields: [
|
192
|
-
{name: 'id', type: 'Number'},
|
193
|
-
{name: 'name', type: 'String'},
|
194
|
-
{name: 'price', type: 'Number', defaultValue: 0,
|
195
|
-
// Use a convert function for field-level validation or transformation
|
196
|
-
convert: value => {
|
197
|
-
if (typeof value !== 'number' || value < 0) {
|
198
|
-
console.warn('Price field must be a non-negative number!');
|
199
|
-
return 0;
|
200
|
-
}
|
201
|
-
return value;
|
202
|
-
}
|
203
|
-
},
|
204
|
-
{name: 'discount', type: 'Number', defaultValue: 0,
|
205
|
-
// Use a convert function for field-level validation or transformation
|
206
|
-
convert: value => {
|
207
|
-
if (typeof value !== 'number' || value < 0 || value > 1) {
|
208
|
-
console.warn('Discount field must be a number between 0 and 1!');
|
209
|
-
return 0;
|
210
|
-
}
|
211
|
-
return value;
|
212
|
-
}
|
213
|
-
},
|
214
|
-
{name: 'discountedPrice', type: 'Number',
|
215
|
-
// Use a calculate function for derived values based on other fields in the record
|
216
|
-
calculate: (data) => {
|
217
|
-
// 'data' contains the raw field values of the current record
|
218
|
-
return data.price * (1 - data.discount);
|
219
|
-
}
|
220
|
-
}
|
221
|
-
]
|
222
|
-
}
|
223
|
-
}
|
224
|
-
|
225
|
-
Neo.setupClass(ProductModel);
|
226
|
-
```
|
227
|
-
|
228
|
-
### Extending `Neo.data.Store`
|
229
|
-
|
230
|
-
Stores manage collections of data records, often using a defined `Model`.
|
231
|
-
|
232
|
-
```javascript readonly
|
233
|
-
import Store from '../../src/data/Store.mjs';
|
234
|
-
import ProductModel from './ProductModel.mjs'; // Assuming ProductModel is in the same directory
|
235
|
-
|
236
|
-
class ProductsStore extends Store {
|
237
|
-
static config = {
|
238
|
-
className: 'App.store.Products',
|
239
|
-
model : ProductModel, // Use our custom ProductModel
|
240
|
-
autoLoad : true,
|
241
|
-
url : '/api/products', // Example API endpoint
|
242
|
-
sorters : [{
|
243
|
-
property : 'name',
|
244
|
-
direction: 'ASC'
|
245
|
-
}]
|
246
|
-
}
|
247
|
-
|
248
|
-
// Custom method to filter by price range
|
249
|
-
filterByPriceRange(min, max) {
|
250
|
-
// The idiomatic way to apply filters is by setting the 'filters' config.
|
251
|
-
// This replaces any existing filters.
|
252
|
-
this.filters = [{
|
253
|
-
property: 'price',
|
254
|
-
operator: '>=',
|
255
|
-
value : min
|
256
|
-
}, {
|
257
|
-
property: 'price',
|
258
|
-
operator: '<=',
|
259
|
-
value : max
|
260
|
-
}];
|
261
|
-
}
|
262
|
-
|
263
|
-
// To add filters without replacing existing ones, you would typically
|
264
|
-
// read the current filters, add new ones, and then set the filters config.
|
265
|
-
// Example (conceptual, not part of the class):
|
266
|
-
/*
|
267
|
-
addPriceRangeFilter(min, max) {
|
268
|
-
const currentFilters = this.filters ? [...this.filters] : [];
|
269
|
-
currentFilters.push({
|
270
|
-
property: 'price',
|
271
|
-
operator: '>=',
|
272
|
-
value : min
|
273
|
-
}, {
|
274
|
-
property: 'price',
|
275
|
-
operator: '<=',
|
276
|
-
value : max
|
277
|
-
});
|
278
|
-
this.filters = currentFilters;
|
279
|
-
}
|
280
|
-
*/
|
281
|
-
}
|
282
|
-
|
283
|
-
Neo.setupClass(ProductsStore);
|
284
|
-
```
|
285
|
-
|
286
|
-
### Extending `Neo.controller.Component`
|
287
|
-
|
288
|
-
Controllers encapsulate logic related to components, often handling events or managing state.
|
289
|
-
|
290
|
-
```javascript readonly
|
291
|
-
import ComponentController from '../../src/controller/Component.mjs';
|
292
|
-
|
293
|
-
class MyCustomController extends ComponentController {
|
294
|
-
static config = {
|
295
|
-
className: 'App.controller.MyCustom',
|
296
|
-
// A reactive property to manage a piece of controller-specific state
|
297
|
-
isActive_: false
|
298
|
-
}
|
299
|
-
|
300
|
-
onConstructed() {
|
301
|
-
super.onConstructed();
|
302
|
-
console.log('MyCustomController constructed!');
|
303
|
-
}
|
304
|
-
|
305
|
-
afterSetIsActive(value, oldValue) {
|
306
|
-
console.log(`Controller active state changed from ${oldValue} to ${value}`);
|
307
|
-
// Perform actions based on active state change
|
308
|
-
if (value) {
|
309
|
-
this.doSomethingActive();
|
310
|
-
} else {
|
311
|
-
this.doSomethingInactive();
|
312
|
-
}
|
313
|
-
}
|
314
|
-
|
315
|
-
doSomethingActive() {
|
316
|
-
console.log('Controller is now active!');
|
317
|
-
// Example: enable a feature, start a timer
|
318
|
-
}
|
319
|
-
|
320
|
-
doSomethingInactive() {
|
321
|
-
console.log('Controller is now inactive!');
|
322
|
-
// Example: disable a feature, clear a timer
|
323
|
-
}
|
324
|
-
}
|
325
|
-
|
326
|
-
Neo.setupClass(MyCustomController);
|
327
|
-
```
|
328
|
-
|
329
|
-
## Conclusion
|
330
|
-
|
331
|
-
The class extension mechanism, coupled with the reactive config system and `Neo.setupClass()`, forms the backbone of development in Neo.mjs. By mastering these principles, you can create highly modular, maintainable, and powerful applications that seamlessly integrate with the framework's core.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
/package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md}
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|