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
@@ -48,6 +48,11 @@ class Collection extends Base {
48
48
  * @member {Boolean} autoSort=true
49
49
  */
50
50
  autoSort: true,
51
+ /**
52
+ * Stores the items.length of the items array in use
53
+ * @member {Number} count_=0
54
+ */
55
+ count_: 0,
51
56
  /**
52
57
  * Use 'primitive' for default filters, use 'advanced' for filters using a filterBy method
53
58
  * which need to iterate over other collection items
@@ -140,6 +145,17 @@ class Collection extends Base {
140
145
  }
141
146
 
142
147
  /**
148
+ * Triggered after the badgePosition config got changed
149
+ * @param {Number} value
150
+ * @param {Number} oldValue
151
+ * @protected
152
+ */
153
+ afterSetCount(value, oldValue) {
154
+ this.fire('countChange', {oldValue, value})
155
+ }
156
+
157
+ /**
158
+ * Triggered after the filters config got changed
143
159
  * @param {Array} value
144
160
  * @param {Array} oldValue
145
161
  * @protected
@@ -158,6 +174,7 @@ class Collection extends Base {
158
174
  }
159
175
 
160
176
  /**
177
+ * Triggered after the items config got changed
161
178
  * @param {Array} value
162
179
  * @param {Array} oldValue
163
180
  * @protected
@@ -174,10 +191,13 @@ class Collection extends Base {
174
191
  item = value[i];
175
192
  me.map.set(item[keyProperty], item)
176
193
  }
194
+
195
+ me.count = len
177
196
  }
178
197
  }
179
198
 
180
199
  /**
200
+ * Triggered after the sorters config got changed
181
201
  * @param {Array} value
182
202
  * @param {Array} oldValue
183
203
  * @protected
@@ -198,6 +218,7 @@ class Collection extends Base {
198
218
  }
199
219
 
200
220
  /**
221
+ * Triggered after the sourceId config got changed
201
222
  * @param {Number|String} value
202
223
  * @param {Number|String} oldValue
203
224
  * @protected
@@ -673,6 +694,7 @@ class Collection extends Base {
673
694
 
674
695
  me.allItems = Neo.create(Collection, {
675
696
  ...Neo.clone(config, true, true),
697
+ id : me.id + '-all',
676
698
  keyProperty: me.keyProperty,
677
699
  sourceId : me.id
678
700
  })
@@ -727,6 +749,8 @@ class Collection extends Base {
727
749
  me.doSort(me.items, true)
728
750
  }
729
751
 
752
+ me.count = me.items.length;
753
+
730
754
  me.fire('filter', {
731
755
  isFiltered: me[isFiltered],
732
756
  items : me.items,
@@ -845,11 +869,12 @@ class Collection extends Base {
845
869
  }
846
870
 
847
871
  /**
848
- * Returns the length of the internal items array
872
+ * Returns the config value of this.count
849
873
  * @returns {Number}
874
+ * @deprecated Use `this.count` directly instead.
850
875
  */
851
876
  getCount() {
852
- return this._items.length
877
+ return this._count || 0 // skipping beforeGetCount() on purpose
853
878
  }
854
879
 
855
880
  /**
@@ -1238,6 +1263,8 @@ class Collection extends Base {
1238
1263
  }
1239
1264
 
1240
1265
  if (me[updatingIndex] === 0) {
1266
+ me.count = me._items.length;
1267
+
1241
1268
  me.fire('mutate', {
1242
1269
  addedItems : toAddArray,
1243
1270
  preventBubbleUp: me.preventBubbleUp,
@@ -10,6 +10,7 @@ import Rectangle from '../util/Rectangle.mjs';
10
10
  import Style from '../util/Style.mjs';
11
11
  import VDomUtil from '../util/VDom.mjs';
12
12
  import VNodeUtil from '../util/VNode.mjs';
13
+ import {isDescriptor} from '../core/ConfigSymbols.mjs';
13
14
 
14
15
  const
15
16
  addUnits = value => value == null ? value : isNaN(value) ? value : `${value}px`,
@@ -378,7 +379,11 @@ class Component extends Base {
378
379
  * @member {Object} vnode_=null
379
380
  * @protected
380
381
  */
381
- vnode_: null,
382
+ vnode_: {
383
+ [isDescriptor]: true,
384
+ value : null,
385
+ isEqual : (a, b) => a === b // vnode trees can be huge, and will get compared by the vdom worker.
386
+ },
382
387
  /**
383
388
  * Shortcut for style.width, defaults to px
384
389
  * @member {Number|String|null} width_=null
@@ -646,21 +651,6 @@ class Component extends Base {
646
651
  }
647
652
  }
648
653
 
649
- /**
650
- * Triggered after the flex config got changed
651
- * @param {Number|String|null} value
652
- * @param {Number|String|null} oldValue
653
- * @protected
654
- */
655
- afterSetFlex(value, oldValue) {
656
- if (!isNaN(value)) {
657
- value = `${value} ${value} 0%`
658
- }
659
-
660
- this.configuredFlex = value;
661
- this.changeVdomRootKey('flex', value)
662
- }
663
-
664
654
  /**
665
655
  * Triggered after the hasUnmountedVdomChanges config got changed
666
656
  * @param {Boolean} value
@@ -2,8 +2,11 @@ import Base from '../core/Base.mjs';
2
2
  import HashHistory from '../util/HashHistory.mjs';
3
3
 
4
4
  const
5
- amountSlashesRegex = /\//g,
6
- routeParamRegex = /{[^\s/]+}/g
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 creating this controller instance,
26
- * neo will set this hash value for us.
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
- * Triggered after the routes config got changed
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(routeParamRegex, '([\\w-.]+)')+'$')
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 onConstructed() {
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
- * Placeholder method which gets triggered when the hash inside the browser url changes
122
- * @param {Object} value
123
- * @param {Object} oldValue
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
- routeKeysLength = routeKeys.length,
137
- arrayParamIds, arrayParamValues, handler, key, paramObject, preHandler, responsePreHandler, result, route;
152
+ bestMatch = null,
153
+ bestMatchKey = null,
154
+ bestMatchParams = null;
138
155
 
139
- while (routeKeysLength > 0 && counter < routeKeysLength && !hasRouteBeenFound) {
140
- key = routeKeys[counter];
141
- handler = null;
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
- arrayParamIds = key.match(routeParamRegex);
149
- arrayParamValues = result.splice(1, result.length - 1);
150
-
151
- if (arrayParamIds && arrayParamIds.length !== arrayParamValues.length) {
152
- throw 'Number of IDs and number of Values do not match'
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
- for (let i = 0; arrayParamIds && i < arrayParamIds.length; i++) {
156
- paramObject[arrayParamIds[i].substring(1, arrayParamIds[i].length - 1)] = arrayParamValues[i]
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
- route = routes[key];
160
-
161
- if (Neo.isString(route)) {
162
- handler = route;
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
- hasRouteBeenFound = true
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
- counter++
173
- }
201
+ let responsePreHandler = true;
174
202
 
175
- // execute
176
- if (hasRouteBeenFound) {
177
203
  if (preHandler) {
178
- responsePreHandler = await me[preHandler]?.call(me, paramObject, value, oldValue)
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, paramObject, value, oldValue)
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 which gets triggered when an invalid route is called
199
- * @param {Object} value
200
- * @param {Object} oldValue
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 amount of slashes
208
- * @param {String} route1
209
- * @param {String} route2
210
- * @returns {Number}
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(amountSlashesRegex) || []).length - (route2.match(amountSlashesRegex)|| []).length
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
- * Applies the observable mixin if needed, grants remote access if needed.
137
- * @param {Object} config={}
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.createId(config.id || me.id);
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
@@ -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 extends Base {
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
- Compare = Neo.setupClass(Compare);
166
+ const ns = Neo.ns('Neo.core', true);
167
+ ns.Compare = Compare;
178
168
 
179
169
  // alias
180
170
  Neo.isEqual = Compare.isEqual;