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/controller/Base.mjs
CHANGED
@@ -2,8 +2,11 @@ import Base from '../core/Base.mjs';
|
|
2
2
|
import HashHistory from '../util/HashHistory.mjs';
|
3
3
|
|
4
4
|
const
|
5
|
-
|
6
|
-
|
5
|
+
regexAmountSlashes = /\//g,
|
6
|
+
// Regex to extract the parameter name from a single route segment (e.g., {*itemId} -> itemId)
|
7
|
+
regexParamNameExtraction = /{(\*|\.\.\.)?([^}]+)}/,
|
8
|
+
// Regex to match route parameters like {paramName}, {*paramName}, or {...paramName}
|
9
|
+
regexRouteParam = /{(\*|\.\.\.)?([^}]+)}/g;
|
7
10
|
|
8
11
|
/**
|
9
12
|
* @class Neo.controller.Base
|
@@ -22,25 +25,33 @@ class Controller extends Base {
|
|
22
25
|
*/
|
23
26
|
ntype: 'controller',
|
24
27
|
/**
|
25
|
-
* If the URL does not contain a hash value when
|
26
|
-
*
|
28
|
+
* If the URL does not contain a hash value when this controller instance is created,
|
29
|
+
* Neo.mjs will automatically set this hash value, ensuring a default route is active.
|
27
30
|
* @member {String|null} defaultHash=null
|
28
31
|
*/
|
29
32
|
defaultHash: null,
|
30
33
|
/**
|
34
|
+
* Specifies the handler method to be invoked when no other defined route matches the URL hash.
|
35
|
+
* This acts as a fallback for unhandled routes.
|
31
36
|
* @member {String|null} defaultRoute=null
|
32
37
|
*/
|
33
38
|
defaultRoute: null,
|
34
39
|
/**
|
40
|
+
* Internal map of compiled regular expressions for each route, used for efficient hash matching.
|
41
|
+
* @protected
|
35
42
|
* @member {Object} handleRoutes={}
|
36
43
|
*/
|
37
44
|
handleRoutes: {},
|
38
45
|
/**
|
46
|
+
* Defines the routing rules for the controller. Keys are route patterns, and values are either
|
47
|
+
* handler method names (String) or objects containing `handler` and optional `preHandler` method names.
|
48
|
+
* Route patterns can include parameters like `{paramName}` and wildcards like `{*paramName}` for nested paths.
|
39
49
|
* @example
|
40
50
|
* routes: {
|
41
51
|
* '/home' : 'handleHomeRoute',
|
42
52
|
* '/users/{userId}' : {handler: 'handleUserRoute', preHandler: 'preHandleUserRoute'},
|
43
53
|
* '/users/{userId}/posts/{postId}': 'handlePostRoute',
|
54
|
+
* '/learn/{*itemId}' : 'onLearnRoute', // Captures nested paths like /learn/gettingstarted/Workspaces
|
44
55
|
* 'default' : 'handleOtherRoutes'
|
45
56
|
* }
|
46
57
|
* @member {Object} routes_={}
|
@@ -49,16 +60,18 @@ class Controller extends Base {
|
|
49
60
|
}
|
50
61
|
|
51
62
|
/**
|
63
|
+
* Creates a new Controller instance and registers its `onHashChange` method
|
64
|
+
* to listen for changes in the browser's URL hash.
|
52
65
|
* @param {Object} config
|
53
66
|
*/
|
54
67
|
construct(config) {
|
55
68
|
super.construct(config);
|
56
|
-
|
57
69
|
HashHistory.on('change', this.onHashChange, this)
|
58
70
|
}
|
59
71
|
|
60
72
|
/**
|
61
|
-
*
|
73
|
+
* Processes the defined routes configuration, compiling route patterns into regular expressions
|
74
|
+
* for efficient matching and sorting them by specificity (more slashes first).
|
62
75
|
* @param {Object} value
|
63
76
|
* @param {Object} oldValue
|
64
77
|
* @protected
|
@@ -78,7 +91,13 @@ class Controller extends Base {
|
|
78
91
|
if (key.toLowerCase() === 'default'){
|
79
92
|
me.defaultRoute = value[key]
|
80
93
|
} else {
|
81
|
-
me.handleRoutes[key] = new RegExp(key.replace(
|
94
|
+
me.handleRoutes[key] = new RegExp(key.replace(regexRouteParam, (match, isWildcard, paramName) => {
|
95
|
+
if (isWildcard || paramName.startsWith('*')) {
|
96
|
+
return '(.*)'
|
97
|
+
} else {
|
98
|
+
return '([\\w-.]+)'
|
99
|
+
}
|
100
|
+
}))
|
82
101
|
}
|
83
102
|
})
|
84
103
|
}
|
@@ -88,21 +107,19 @@ class Controller extends Base {
|
|
88
107
|
*/
|
89
108
|
destroy(...args) {
|
90
109
|
HashHistory.un('change', this.onHashChange, this);
|
91
|
-
|
92
110
|
super.destroy(...args)
|
93
111
|
}
|
94
112
|
|
95
113
|
/**
|
96
|
-
*
|
114
|
+
* @returns {Promise<void>}
|
97
115
|
*/
|
98
|
-
async
|
116
|
+
async initAsync() {
|
117
|
+
await super.initAsync();
|
118
|
+
|
99
119
|
let me = this,
|
100
120
|
{defaultHash, windowId} = me,
|
101
121
|
currentHash = HashHistory.first(windowId);
|
102
122
|
|
103
|
-
// get outside the construction chain => a related cmp & vm has to be constructed too
|
104
|
-
await me.timeout(1);
|
105
|
-
|
106
123
|
if (currentHash) {
|
107
124
|
if (currentHash.windowId === windowId) {
|
108
125
|
await me.onHashChange(currentHash, null)
|
@@ -118,9 +135,10 @@ class Controller extends Base {
|
|
118
135
|
}
|
119
136
|
|
120
137
|
/**
|
121
|
-
*
|
122
|
-
*
|
123
|
-
* @param {Object}
|
138
|
+
* Handles changes in the browser's URL hash. It identifies the most specific matching route
|
139
|
+
* and dispatches the corresponding handler, optionally executing a preHandler first.
|
140
|
+
* @param {Object} value - The new hash history entry.
|
141
|
+
* @param {Object} oldValue - The previous hash history entry.
|
124
142
|
*/
|
125
143
|
async onHashChange(value, oldValue) {
|
126
144
|
// We only want to trigger hash changes for the same browser window (SharedWorker context)
|
@@ -129,63 +147,67 @@ class Controller extends Base {
|
|
129
147
|
}
|
130
148
|
|
131
149
|
let me = this,
|
132
|
-
counter = 0,
|
133
|
-
hasRouteBeenFound = false,
|
134
150
|
{handleRoutes, routes} = me,
|
135
151
|
routeKeys = Object.keys(handleRoutes),
|
136
|
-
|
137
|
-
|
152
|
+
bestMatch = null,
|
153
|
+
bestMatchKey = null,
|
154
|
+
bestMatchParams = null;
|
138
155
|
|
139
|
-
|
140
|
-
key
|
141
|
-
|
142
|
-
preHandler = null;
|
143
|
-
responsePreHandler = null;
|
144
|
-
paramObject = {};
|
145
|
-
result = value.hashString.match(handleRoutes[key]);
|
156
|
+
for (let i = 0; i < routeKeys.length; i++) {
|
157
|
+
const key = routeKeys[i];
|
158
|
+
const result = value.hashString.match(handleRoutes[key]);
|
146
159
|
|
147
160
|
if (result) {
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
161
|
+
const
|
162
|
+
arrayParamIds = key.match(regexRouteParam),
|
163
|
+
arrayParamValues = result.splice(1, result.length - 1),
|
164
|
+
paramObject = {};
|
165
|
+
|
166
|
+
if (arrayParamIds) {
|
167
|
+
for (let j = 0; j < arrayParamIds.length; j++) {
|
168
|
+
const paramMatch = arrayParamIds[j].match(regexParamNameExtraction);
|
169
|
+
|
170
|
+
if (paramMatch) {
|
171
|
+
const paramName = paramMatch[2];
|
172
|
+
paramObject[paramName] = arrayParamValues[j];
|
173
|
+
}
|
174
|
+
}
|
153
175
|
}
|
154
176
|
|
155
|
-
|
156
|
-
|
177
|
+
// Logic to determine the best matching route:
|
178
|
+
// 1. Prioritize routes that match a longer string (more specific match).
|
179
|
+
// 2. If lengths are equal, prioritize routes with more slashes (deeper nesting).
|
180
|
+
if (!bestMatch || (result[0].length > bestMatch[0].length) ||
|
181
|
+
(result[0].length === bestMatch[0].length && (key.match(regexAmountSlashes) || []).length > (bestMatchKey.match(regexAmountSlashes) || []).length)) {
|
182
|
+
bestMatch = result;
|
183
|
+
bestMatchKey = key;
|
184
|
+
bestMatchParams = paramObject;
|
157
185
|
}
|
186
|
+
}
|
187
|
+
}
|
158
188
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
responsePreHandler = true
|
164
|
-
} else if (Neo.isObject(route)) {
|
165
|
-
handler = route.handler;
|
166
|
-
preHandler = route.preHandler
|
167
|
-
}
|
189
|
+
if (bestMatch) {
|
190
|
+
const route = routes[bestMatchKey];
|
191
|
+
let handler = null,
|
192
|
+
preHandler = null;
|
168
193
|
|
169
|
-
|
194
|
+
if (Neo.isString(route)) {
|
195
|
+
handler = route
|
196
|
+
} else if (Neo.isObject(route)) {
|
197
|
+
handler = route.handler;
|
198
|
+
preHandler = route.preHandler
|
170
199
|
}
|
171
200
|
|
172
|
-
|
173
|
-
}
|
201
|
+
let responsePreHandler = true;
|
174
202
|
|
175
|
-
// execute
|
176
|
-
if (hasRouteBeenFound) {
|
177
203
|
if (preHandler) {
|
178
|
-
responsePreHandler = await me[preHandler]?.call(me,
|
179
|
-
} else {
|
180
|
-
responsePreHandler = true
|
204
|
+
responsePreHandler = await me[preHandler]?.call(me, bestMatchParams, value, oldValue)
|
181
205
|
}
|
182
206
|
|
183
207
|
if (responsePreHandler) {
|
184
|
-
await me[handler]?.call(me,
|
208
|
+
await me[handler]?.call(me, bestMatchParams, value, oldValue)
|
185
209
|
}
|
186
|
-
}
|
187
|
-
|
188
|
-
if (routeKeys.length > 0 && !hasRouteBeenFound) {
|
210
|
+
} else {
|
189
211
|
if (me.defaultRoute) {
|
190
212
|
me[me.defaultRoute]?.(value, oldValue)
|
191
213
|
} else {
|
@@ -195,22 +217,24 @@ class Controller extends Base {
|
|
195
217
|
}
|
196
218
|
|
197
219
|
/**
|
198
|
-
* Placeholder method
|
199
|
-
*
|
200
|
-
* @param {Object}
|
220
|
+
* Placeholder method invoked when no matching route is found for the current URL hash.
|
221
|
+
* Controllers can override this to implement custom behavior for unhandled routes.
|
222
|
+
* @param {Object} value - The current hash history entry.
|
223
|
+
* @param {Object} oldValue - The previous hash history entry.
|
201
224
|
*/
|
202
225
|
onNoRouteFound(value, oldValue) {
|
203
226
|
|
204
227
|
}
|
205
228
|
|
206
229
|
/**
|
207
|
-
* Internal helper method to sort routes by their
|
208
|
-
*
|
209
|
-
* @param {String}
|
210
|
-
* @
|
230
|
+
* Internal helper method to sort routes by their specificity.
|
231
|
+
* Routes with more slashes are considered more specific and are prioritized.
|
232
|
+
* @param {String} route1 - The first route string to compare.
|
233
|
+
* @param {String} route2 - The second route string to compare.
|
234
|
+
* @returns {Number} A negative value if route1 is more specific, a positive value if route2 is more specific, or 0 if they have equal specificity.
|
211
235
|
*/
|
212
236
|
#sortRoutes(route1, route2) {
|
213
|
-
return (route1.match(
|
237
|
+
return (route1.match(regexAmountSlashes) || []).length - (route2.match(regexAmountSlashes)|| []).length
|
214
238
|
}
|
215
239
|
}
|
216
240
|
|
package/src/core/Base.mjs
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
import {buffer, debounce, intercept, resolveCallback, throttle} from '../util/Function.mjs';
|
2
|
+
import Compare from '../core/Compare.mjs';
|
3
|
+
import Util from '../core/Util.mjs';
|
4
|
+
import Config from './Config.mjs';
|
5
|
+
import {isDescriptor} from './ConfigSymbols.mjs';
|
2
6
|
import IdGenerator from './IdGenerator.mjs'
|
3
7
|
|
4
8
|
const configSymbol = Symbol.for('configSymbol'),
|
@@ -125,6 +129,12 @@ class Base {
|
|
125
129
|
remote_: null
|
126
130
|
}
|
127
131
|
|
132
|
+
/**
|
133
|
+
* A private field to store the Config controller instances.
|
134
|
+
* @member {Object} #configs={}
|
135
|
+
* @private
|
136
|
+
*/
|
137
|
+
#configs = {};
|
128
138
|
/**
|
129
139
|
* Internal cache for all timeout ids when using this.timeout()
|
130
140
|
* @member {Number[]} timeoutIds=[]
|
@@ -133,8 +143,39 @@ class Base {
|
|
133
143
|
#timeoutIds = []
|
134
144
|
|
135
145
|
/**
|
136
|
-
*
|
137
|
-
*
|
146
|
+
* The main initializer for all Neo.mjs classes, invoked by `Neo.create()`.
|
147
|
+
* NOTE: This is not the native `constructor()`, which is called without arguments by `Neo.create()` first.
|
148
|
+
*
|
149
|
+
* This method orchestrates the entire instance initialization process, including
|
150
|
+
* the setup of the powerful and flexible config system.
|
151
|
+
*
|
152
|
+
* The `config` parameter is a single object that can contain different types of properties,
|
153
|
+
* which are processed in a specific order to ensure consistency and predictability:
|
154
|
+
*
|
155
|
+
* 1. **Public Class Fields & Other Properties:** Any key in the `config` object that is NOT
|
156
|
+
* defined in the class's `static config` hierarchy is considered a public field or a
|
157
|
+
* dynamic property. These are assigned directly to the instance (`this.myField = value`)
|
158
|
+
* at the very beginning. This is crucial so that subsequent config hooks (like `afterSet*`)
|
159
|
+
* can access their latest values.
|
160
|
+
*
|
161
|
+
* 2. **Reactive Configs:** A property is considered reactive if it is defined with a trailing
|
162
|
+
* underscore (e.g., `myValue_`) in the `static config` of **any class in the inheritance
|
163
|
+
* chain**. Subclasses can provide new default values for these configs without the
|
164
|
+
* underscore, and they will still be reactive. Their values are applied via generated
|
165
|
+
* setters, triggering `beforeSet*` and `afterSet*` hooks, and they are wrapped in a
|
166
|
+
* `Neo.core.Config` instance to enable subscription-based reactivity.
|
167
|
+
*
|
168
|
+
* 3. **Non-Reactive Configs:** Properties defined in `static config` without a trailing
|
169
|
+
* underscore in their entire inheritance chain. Their default values are applied directly
|
170
|
+
* to the class **prototype**, making them shared across all instances and allowing for
|
171
|
+
* run-time modifications (prototypal inheritance). When a new value is passed to this
|
172
|
+
* method, it creates an instance-specific property that shadows the prototype value.
|
173
|
+
*
|
174
|
+
* This method also initializes the observable mixin (if applicable) and schedules asynchronous
|
175
|
+
* logic like `initAsync()` (which handles remote method access) to run after the synchronous
|
176
|
+
* construction chain is complete.
|
177
|
+
*
|
178
|
+
* @param {Object} config={} The initial configuration object for the instance.
|
138
179
|
*/
|
139
180
|
construct(config={}) {
|
140
181
|
let me = this;
|
@@ -152,13 +193,9 @@ class Base {
|
|
152
193
|
}
|
153
194
|
});
|
154
195
|
|
155
|
-
me.
|
196
|
+
me.id = config.id || IdGenerator.getId(this.getIdKey());
|
156
197
|
delete config.id;
|
157
198
|
|
158
|
-
if (me.constructor.config) {
|
159
|
-
delete me.constructor.config.id
|
160
|
-
}
|
161
|
-
|
162
199
|
me.getStaticConfig('observable') && me.initObservable(config);
|
163
200
|
|
164
201
|
// assign class field values prior to configs
|
@@ -342,16 +379,6 @@ class Base {
|
|
342
379
|
this.__proto__.constructor.overwrittenMethods[methodName].call(this, ...args)
|
343
380
|
}
|
344
381
|
|
345
|
-
/**
|
346
|
-
* Uses the IdGenerator to create an id if a static one is not explicitly set.
|
347
|
-
* Registers the instance to manager.Instance if this one is already created,
|
348
|
-
* otherwise stores it inside a tmp map.
|
349
|
-
* @param {String} id
|
350
|
-
*/
|
351
|
-
createId(id) {
|
352
|
-
this.id = id || IdGenerator.getId(this.getIdKey())
|
353
|
-
}
|
354
|
-
|
355
382
|
/**
|
356
383
|
* Unregisters this instance from Neo.manager.Instance
|
357
384
|
* and removes all object entries from this instance
|
@@ -386,6 +413,22 @@ class Base {
|
|
386
413
|
me.isDestroyed = true
|
387
414
|
}
|
388
415
|
|
416
|
+
/**
|
417
|
+
* A public method to access the underlying Config controller.
|
418
|
+
* This enables advanced interactions like subscriptions.
|
419
|
+
* @param {String} key The name of the config property (e.g., 'items').
|
420
|
+
* @returns {Config|undefined} The Config instance, or undefined if not found.
|
421
|
+
*/
|
422
|
+
getConfig(key) {
|
423
|
+
let me = this;
|
424
|
+
|
425
|
+
if (!me.#configs[key] && me.isConfig(key)) {
|
426
|
+
me.#configs[key] = new Config()
|
427
|
+
}
|
428
|
+
|
429
|
+
return me.#configs[key]
|
430
|
+
}
|
431
|
+
|
389
432
|
/**
|
390
433
|
* Used inside createId() as the default value passed to the IdGenerator.
|
391
434
|
* Override this method as needed.
|
@@ -443,6 +486,7 @@ class Base {
|
|
443
486
|
|
444
487
|
me.isConfiguring = true;
|
445
488
|
Object.assign(me[configSymbol], me.mergeConfig(config, preventOriginalConfig));
|
489
|
+
delete me[configSymbol].id;
|
446
490
|
me.processConfigs();
|
447
491
|
me.isConfiguring = false;
|
448
492
|
}
|
@@ -475,6 +519,17 @@ class Base {
|
|
475
519
|
return !this.isDestroyed
|
476
520
|
}
|
477
521
|
|
522
|
+
/**
|
523
|
+
* @param {String} key
|
524
|
+
* @returns {Boolean}
|
525
|
+
*/
|
526
|
+
isConfig(key) {
|
527
|
+
// A config is considered "reactive" if it has a generated property setter
|
528
|
+
// AND it is present as a defined config in the merged static config hierarchy.
|
529
|
+
// Neo.setupClass() removes the underscore from the static config keys.
|
530
|
+
return Neo.hasPropertySetter(this, key) && (key in this.constructor.config);
|
531
|
+
}
|
532
|
+
|
478
533
|
/**
|
479
534
|
* Override this method to change the order configs are applied to this instance.
|
480
535
|
* @param {Object} config
|
package/src/core/Compare.mjs
CHANGED
@@ -1,18 +1,7 @@
|
|
1
|
-
import Base from '../core/Base.mjs';
|
2
|
-
|
3
1
|
/**
|
4
2
|
* @class Neo.core.Compare
|
5
|
-
* @extends Neo.core.Base
|
6
3
|
*/
|
7
|
-
class Compare
|
8
|
-
static config = {
|
9
|
-
/**
|
10
|
-
* @member {String} className='Neo.core.Compare'
|
11
|
-
* @protected
|
12
|
-
*/
|
13
|
-
className: 'Neo.core.Compare'
|
14
|
-
}
|
15
|
-
|
4
|
+
class Compare {
|
16
5
|
/**
|
17
6
|
* Storing the comparison method names by data type
|
18
7
|
* @member {Object} map
|
@@ -174,7 +163,8 @@ class Compare extends Base {
|
|
174
163
|
}
|
175
164
|
}
|
176
165
|
|
177
|
-
|
166
|
+
const ns = Neo.ns('Neo.core', true);
|
167
|
+
ns.Compare = Compare;
|
178
168
|
|
179
169
|
// alias
|
180
170
|
Neo.isEqual = Compare.isEqual;
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import {isDescriptor} from './ConfigSymbols.mjs';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @src/util/ClassSystem.mjs Neo.core.Config
|
5
|
+
* @private
|
6
|
+
* @internal
|
7
|
+
*
|
8
|
+
* Represents an observable container for a config property.
|
9
|
+
* This class manages the value of a config, its subscribers, and custom behaviors
|
10
|
+
* like merge strategies and equality checks defined via a descriptor object.
|
11
|
+
*
|
12
|
+
* The primary purpose of this class is to enable fine-grained reactivity and
|
13
|
+
* decoupled cross-instance state sharing within the Neo.mjs framework.
|
14
|
+
*/
|
15
|
+
class Config {
|
16
|
+
/**
|
17
|
+
* The internal value of the config property.
|
18
|
+
* @private
|
19
|
+
* @apps/portal/view/about/MemberContainer.mjs {any} #value
|
20
|
+
*/
|
21
|
+
#value;
|
22
|
+
|
23
|
+
/**
|
24
|
+
* A Set to store callback functions that subscribe to changes in this config's value.
|
25
|
+
* @private
|
26
|
+
* @apps/portal/view/about/MemberContainer.mjs {Set<Function>} #subscribers
|
27
|
+
*/
|
28
|
+
#subscribers = new Set();
|
29
|
+
|
30
|
+
/**
|
31
|
+
* The strategy to use when merging new values into this config.
|
32
|
+
* Defaults to 'deep'. Can be overridden via a descriptor.
|
33
|
+
* @apps/portal/view/about/MemberContainer.mjs {string} mergeStrategy
|
34
|
+
*/
|
35
|
+
mergeStrategy = 'deep';
|
36
|
+
|
37
|
+
/**
|
38
|
+
* The function used to compare new and old values for equality.
|
39
|
+
* Defaults to `Neo.isEqual`. Can be overridden via a descriptor.
|
40
|
+
* @apps/portal/view/about/MemberContainer.mjs {Function} isEqual
|
41
|
+
*/
|
42
|
+
isEqual = Neo.isEqual;
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Creates an instance of Config.
|
46
|
+
* @param {any|Object} configObject - The initial value for the config.
|
47
|
+
*/
|
48
|
+
constructor(configObject) {
|
49
|
+
if (Neo.isObject(configObject) && configObject[isDescriptor] === true) {
|
50
|
+
this.initDescriptor(configObject)
|
51
|
+
} else {
|
52
|
+
this.#value = configObject
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* Gets the current value of the config property.
|
58
|
+
* @returns {any} The current value.
|
59
|
+
*/
|
60
|
+
get() {
|
61
|
+
return this.#value;
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Initializes the `Config` instance using a descriptor object.
|
66
|
+
* Extracts `mergeStrategy` and `isEqual` from the descriptor.
|
67
|
+
* The internal `#value` is NOT set by this method.
|
68
|
+
* @param {Object} descriptor - The descriptor object for the config.
|
69
|
+
* @param {any} descriptor.value - The default value for the config (not set by this method).
|
70
|
+
* @param {string} [descriptor.merge='deep'] - The merge strategy.
|
71
|
+
* @param {Function} [descriptor.isEqual=Neo.isEqual] - The equality comparison function.
|
72
|
+
*/
|
73
|
+
initDescriptor({isEqual, merge, value}) {
|
74
|
+
let me = this;
|
75
|
+
|
76
|
+
me.#value = value
|
77
|
+
me.mergeStrategy = merge || me.mergeStrategy;
|
78
|
+
me.isEqual = isEqual || me.isEqual;
|
79
|
+
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Notifies all subscribed callbacks about a change in the config's value.
|
83
|
+
* @param {any} newValue - The new value of the config.
|
84
|
+
* @param {any} oldValue - The old value of the config.
|
85
|
+
*/
|
86
|
+
notify(newValue, oldValue) {
|
87
|
+
for (const callback of this.#subscribers) {
|
88
|
+
callback(newValue, oldValue);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Sets a new value for the config property.
|
94
|
+
* This method performs an equality check using `this.isEqual` before updating the value.
|
95
|
+
* If the value has changed, it updates `#value` and notifies all subscribers.
|
96
|
+
* @param {any} newValue - The new value to set.
|
97
|
+
* @returns {Boolean} True if the value changed, false otherwise.
|
98
|
+
*/
|
99
|
+
set(newValue) {
|
100
|
+
if (newValue === undefined) return false; // Preserve original behavior for undefined
|
101
|
+
|
102
|
+
const
|
103
|
+
me = this,
|
104
|
+
oldValue = me.#value;
|
105
|
+
|
106
|
+
// The setter automatically uses the configured equality check
|
107
|
+
if (!me.isEqual(newValue, oldValue)) {
|
108
|
+
me.#value = newValue;
|
109
|
+
me.notify(newValue, oldValue);
|
110
|
+
return true
|
111
|
+
}
|
112
|
+
|
113
|
+
return false
|
114
|
+
}
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Sets the internal value of the config property directly, without performing
|
118
|
+
* an equality check or notifying subscribers.
|
119
|
+
* This method is intended for internal framework use where direct assignment
|
120
|
+
* is necessary (e.g., during initial setup or specific internal optimizations).
|
121
|
+
* @param {any} newValue - The new value to set directly.
|
122
|
+
*/
|
123
|
+
setRaw(newValue) {
|
124
|
+
this.#value = newValue
|
125
|
+
}
|
126
|
+
|
127
|
+
/**
|
128
|
+
* Subscribes a callback function to changes in this config's value.
|
129
|
+
* The callback will be invoked with `(newValue, oldValue)` whenever the config changes.
|
130
|
+
* @param {Function} callback - The function to call when the config value changes.
|
131
|
+
* @returns {Function} A cleanup function to unsubscribe the callback.
|
132
|
+
*/
|
133
|
+
subscribe(callback) {
|
134
|
+
this.#subscribers.add(callback);
|
135
|
+
return () => this.#subscribers.delete(callback)
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
export default Config;
|
package/src/core/Util.mjs
CHANGED
@@ -1,10 +1,7 @@
|
|
1
|
-
import Base from './Base.mjs';
|
2
|
-
|
3
1
|
/**
|
4
2
|
* @class Neo.core.Util
|
5
|
-
* @extends Neo.core.Base
|
6
3
|
*/
|
7
|
-
class Util
|
4
|
+
class Util {
|
8
5
|
/**
|
9
6
|
* A regex to remove camel case syntax
|
10
7
|
* @member {RegExp} decamelRegEx=/([a-z])([A-Z])/g
|
@@ -13,19 +10,6 @@ class Util extends Base {
|
|
13
10
|
*/
|
14
11
|
static decamelRegEx = /([a-z])([A-Z])/g
|
15
12
|
|
16
|
-
static config = {
|
17
|
-
/**
|
18
|
-
* @member {String} className='Neo.core.Util'
|
19
|
-
* @protected
|
20
|
-
*/
|
21
|
-
className: 'Neo.core.Util',
|
22
|
-
/**
|
23
|
-
* @member {String} ntype='core-util'
|
24
|
-
* @protected
|
25
|
-
*/
|
26
|
-
ntype: 'core-util'
|
27
|
-
}
|
28
|
-
|
29
13
|
/**
|
30
14
|
* @param {Object} scope
|
31
15
|
* @param {String[]} values
|
@@ -229,7 +213,8 @@ class Util extends Base {
|
|
229
213
|
}
|
230
214
|
}
|
231
215
|
|
232
|
-
|
216
|
+
const ns = Neo.ns('Neo.core', true);
|
217
|
+
ns.Util = Util;
|
233
218
|
|
234
219
|
// aliases
|
235
220
|
Neo.applyFromNs(Neo, Util, {
|
@@ -81,7 +81,7 @@ class RecordFactory extends Base {
|
|
81
81
|
return this[dataSymbol][fieldName]
|
82
82
|
},
|
83
83
|
set(value) {
|
84
|
-
|
84
|
+
this.notifyChange({
|
85
85
|
fields: {[fieldPath]: instance.parseRecordValue({record: this, field, value})},
|
86
86
|
model,
|
87
87
|
record: this
|
@@ -193,6 +193,25 @@ class RecordFactory extends Base {
|
|
193
193
|
return null
|
194
194
|
}
|
195
195
|
|
196
|
+
/**
|
197
|
+
* The single source of truth for record field changes.
|
198
|
+
* Executes instance.setRecordFields(), and can get used via:
|
199
|
+
* - Neo.util.Function:createSequence()
|
200
|
+
* - Neo.util.Function:intercept(),
|
201
|
+
* to "listen" to field changes
|
202
|
+
* @param {Object} data
|
203
|
+
* @param {Object} data.fields
|
204
|
+
* @param {Neo.data.Model} data.model
|
205
|
+
* @param {Object} data.record
|
206
|
+
* @param {Boolean} silent=false
|
207
|
+
* @returns {Object}
|
208
|
+
*/
|
209
|
+
notifyChange(data, silent=false) {
|
210
|
+
const param = {...data, silent}
|
211
|
+
instance.setRecordFields(param);
|
212
|
+
return param
|
213
|
+
}
|
214
|
+
|
196
215
|
/**
|
197
216
|
* Bulk-update multiple record fields at once
|
198
217
|
* @param {Object} fields
|
@@ -207,7 +226,7 @@ class RecordFactory extends Base {
|
|
207
226
|
* @param {Object} fields
|
208
227
|
*/
|
209
228
|
set(fields) {
|
210
|
-
|
229
|
+
this.notifyChange({fields, model, record: this})
|
211
230
|
}
|
212
231
|
|
213
232
|
/**
|
@@ -225,7 +244,7 @@ class RecordFactory extends Base {
|
|
225
244
|
* @param {Object} fields
|
226
245
|
*/
|
227
246
|
setSilent(fields) {
|
228
|
-
|
247
|
+
this.notifyChange({fields, model, record: this}, true)
|
229
248
|
}
|
230
249
|
|
231
250
|
/**
|