neo.mjs 10.0.0-beta.1 → 10.0.0-beta.3

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 (149) hide show
  1. package/ServiceWorker.mjs +2 -2
  2. package/apps/colors/view/GridContainer.mjs +1 -1
  3. package/apps/covid/view/AttributionComponent.mjs +1 -1
  4. package/apps/covid/view/HeaderContainer.mjs +6 -6
  5. package/apps/covid/view/MainContainerController.mjs +5 -5
  6. package/apps/covid/view/TableContainerController.mjs +1 -1
  7. package/apps/covid/view/country/Gallery.mjs +13 -13
  8. package/apps/covid/view/country/Helix.mjs +13 -13
  9. package/apps/covid/view/country/HistoricalDataTable.mjs +1 -1
  10. package/apps/email/view/Viewport.mjs +2 -2
  11. package/apps/form/view/FormPageContainer.mjs +2 -3
  12. package/apps/form/view/SideNavList.mjs +1 -1
  13. package/apps/portal/index.html +1 -1
  14. package/apps/portal/resources/data/examples_dist_esm.json +1 -1
  15. package/apps/portal/resources/data/examples_dist_prod.json +2 -2
  16. package/apps/portal/view/HeaderToolbar.mjs +3 -3
  17. package/apps/portal/view/about/Container.mjs +2 -2
  18. package/apps/portal/view/about/MemberContainer.mjs +3 -3
  19. package/apps/portal/view/blog/List.mjs +7 -7
  20. package/apps/portal/view/examples/List.mjs +4 -4
  21. package/apps/portal/view/home/ContentBox.mjs +2 -2
  22. package/apps/portal/view/home/FeatureSection.mjs +3 -3
  23. package/apps/portal/view/home/FooterContainer.mjs +7 -7
  24. package/apps/portal/view/home/parts/AfterMath.mjs +3 -3
  25. package/apps/portal/view/home/parts/MainNeo.mjs +3 -3
  26. package/apps/portal/view/home/parts/References.mjs +6 -6
  27. package/apps/portal/view/learn/ContentComponent.mjs +18 -11
  28. package/apps/portal/view/learn/PageSectionsContainer.mjs +1 -1
  29. package/apps/portal/view/learn/PageSectionsList.mjs +2 -2
  30. package/apps/portal/view/services/Component.mjs +16 -16
  31. package/apps/realworld/view/FooterComponent.mjs +1 -1
  32. package/apps/realworld/view/HeaderComponent.mjs +8 -8
  33. package/apps/realworld/view/HomeComponent.mjs +6 -6
  34. package/apps/realworld/view/article/CommentComponent.mjs +4 -4
  35. package/apps/realworld/view/article/Component.mjs +14 -14
  36. package/apps/realworld/view/article/CreateCommentComponent.mjs +3 -3
  37. package/apps/realworld/view/article/CreateComponent.mjs +3 -3
  38. package/apps/realworld/view/article/PreviewComponent.mjs +1 -1
  39. package/apps/realworld/view/article/TagListComponent.mjs +2 -2
  40. package/apps/realworld/view/user/ProfileComponent.mjs +8 -8
  41. package/apps/realworld/view/user/SettingsComponent.mjs +4 -4
  42. package/apps/realworld/view/user/SignUpComponent.mjs +4 -4
  43. package/apps/realworld2/view/FooterComponent.mjs +1 -1
  44. package/apps/realworld2/view/HomeContainer.mjs +3 -3
  45. package/apps/realworld2/view/article/DetailsContainer.mjs +1 -1
  46. package/apps/realworld2/view/article/PreviewComponent.mjs +7 -7
  47. package/apps/realworld2/view/article/TagListComponent.mjs +2 -2
  48. package/apps/realworld2/view/user/ProfileContainer.mjs +1 -1
  49. package/apps/route/view/center/CardAdministration.mjs +2 -2
  50. package/apps/route/view/center/CardAdministrationDenied.mjs +1 -1
  51. package/apps/route/view/center/CardContact.mjs +2 -2
  52. package/apps/route/view/center/CardHome.mjs +1 -1
  53. package/apps/route/view/center/CardSection1.mjs +1 -1
  54. package/apps/route/view/center/CardSection2.mjs +1 -1
  55. package/apps/sharedcovid/view/AttributionComponent.mjs +1 -1
  56. package/apps/sharedcovid/view/HeaderContainer.mjs +6 -6
  57. package/apps/sharedcovid/view/MainContainerController.mjs +5 -5
  58. package/apps/sharedcovid/view/TableContainerController.mjs +1 -1
  59. package/apps/sharedcovid/view/country/Gallery.mjs +13 -13
  60. package/apps/sharedcovid/view/country/Helix.mjs +13 -13
  61. package/apps/sharedcovid/view/country/HistoricalDataTable.mjs +1 -1
  62. package/apps/shareddialog/childapps/shareddialog2/view/MainContainer.mjs +1 -1
  63. package/apps/shareddialog/view/MainContainer.mjs +1 -1
  64. package/buildScripts/createApp.mjs +2 -2
  65. package/learn/Glossary.md +261 -0
  66. package/learn/README.md +9 -14
  67. package/learn/benefits/ConfigSystem.md +536 -26
  68. package/learn/benefits/Effort.md +47 -2
  69. package/learn/benefits/Features.md +50 -32
  70. package/learn/benefits/FormsEngine.md +54 -24
  71. package/learn/benefits/MultiWindow.md +31 -5
  72. package/learn/benefits/Quick.md +45 -12
  73. package/learn/benefits/RPCLayer.md +75 -0
  74. package/learn/benefits/Speed.md +17 -12
  75. package/learn/guides/Collections.md +436 -0
  76. package/learn/guides/ConfigSystemDeepDive.md +280 -0
  77. package/learn/guides/CustomComponents.md +256 -14
  78. package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +17 -17
  79. package/learn/guides/ExtendingNeoClasses.md +331 -0
  80. package/learn/guides/Forms.md +449 -1
  81. package/learn/guides/InstanceLifecycle.md +295 -1
  82. package/learn/guides/Layouts.md +246 -1
  83. package/learn/guides/MainThreadAddons.md +475 -0
  84. package/learn/guides/Records.md +286 -0
  85. package/learn/guides/WorkingWithVDom.md +14 -14
  86. package/learn/guides/form_fields/ComboBox.md +241 -0
  87. package/learn/tree.json +57 -51
  88. package/package.json +2 -2
  89. package/resources/scss/src/apps/portal/learn/ContentComponent.scss +9 -0
  90. package/src/DefaultConfig.mjs +2 -2
  91. package/src/Main.mjs +8 -7
  92. package/src/Neo.mjs +16 -2
  93. package/src/button/Base.mjs +2 -2
  94. package/src/calendar/view/SettingsContainer.mjs +2 -2
  95. package/src/calendar/view/YearComponent.mjs +9 -9
  96. package/src/calendar/view/calendars/ColorsList.mjs +1 -1
  97. package/src/calendar/view/calendars/List.mjs +1 -1
  98. package/src/calendar/view/month/Component.mjs +15 -15
  99. package/src/calendar/view/week/Component.mjs +12 -12
  100. package/src/calendar/view/week/EventDragZone.mjs +4 -4
  101. package/src/calendar/view/week/TimeAxisComponent.mjs +3 -3
  102. package/src/component/Base.mjs +17 -2
  103. package/src/component/Carousel.mjs +2 -2
  104. package/src/component/Chip.mjs +3 -3
  105. package/src/component/Circle.mjs +2 -2
  106. package/src/component/DateSelector.mjs +8 -8
  107. package/src/component/Helix.mjs +1 -1
  108. package/src/component/Label.mjs +3 -18
  109. package/src/component/Legend.mjs +3 -3
  110. package/src/component/MagicMoveText.mjs +6 -14
  111. package/src/component/Process.mjs +3 -3
  112. package/src/component/Progress.mjs +1 -1
  113. package/src/component/StatusBadge.mjs +2 -2
  114. package/src/component/Timer.mjs +2 -2
  115. package/src/component/Toast.mjs +5 -3
  116. package/src/container/AccordionItem.mjs +2 -2
  117. package/src/container/Base.mjs +1 -1
  118. package/src/core/Base.mjs +18 -2
  119. package/src/date/DayViewComponent.mjs +2 -2
  120. package/src/date/SelectorContainer.mjs +1 -1
  121. package/src/form/field/CheckBox.mjs +4 -4
  122. package/src/form/field/ComboBox.mjs +6 -1
  123. package/src/form/field/FileUpload.mjs +25 -39
  124. package/src/form/field/Range.mjs +1 -1
  125. package/src/form/field/Text.mjs +3 -3
  126. package/src/form/field/TextArea.mjs +2 -3
  127. package/src/grid/Body.mjs +6 -2
  128. package/src/list/Color.mjs +2 -2
  129. package/src/main/DeltaUpdates.mjs +157 -98
  130. package/src/main/addon/AmCharts.mjs +53 -73
  131. package/src/main/addon/Base.mjs +11 -0
  132. package/src/main/addon/MonacoEditor.mjs +31 -58
  133. package/src/manager/ClassHierarchy.mjs +114 -0
  134. package/src/menu/List.mjs +1 -1
  135. package/src/plugin/Popover.mjs +2 -2
  136. package/src/sitemap/Component.mjs +1 -1
  137. package/src/table/Body.mjs +6 -2
  138. package/src/tooltip/Base.mjs +1 -6
  139. package/src/tree/Accordion.mjs +3 -3
  140. package/src/vdom/Helper.mjs +21 -19
  141. package/src/worker/App.mjs +1 -2
  142. package/src/worker/Base.mjs +6 -4
  143. package/src/worker/Canvas.mjs +2 -3
  144. package/src/worker/Data.mjs +5 -7
  145. package/src/worker/Task.mjs +2 -3
  146. package/src/worker/VDom.mjs +3 -4
  147. package/src/worker/mixin/RemoteMethodAccess.mjs +4 -1
  148. package/learn/guides/MainThreadAddonExample.md +0 -15
  149. package/learn/guides/MainThreadAddonIntro.md +0 -44
@@ -0,0 +1,286 @@
1
+ In Neo.mjs, a **Record** is a super lightweight, reactive JavaScript object, dynamically created and structured
2
+ according to a `Neo.data.Model`. Records provide a powerful way to manage application data with built-in features like
3
+ data validation, type conversion, dirty tracking, and seamless integration with `Neo.data.Store`.
4
+
5
+ This guide will cover:
6
+
7
+ - **What is a Record?**: Understanding the concept and its benefits.
8
+ - **`Neo.data.Model`**: Defining the structure and behavior of your records.
9
+ - **`Neo.data.RecordFactory`**: The engine behind reactive record creation.
10
+ - **Record Fields**: Data types, default values, mapping, and custom logic.
11
+ - **Reactivity and Dirty Tracking**: How records respond to changes and track their state.
12
+ - **Interaction with `Neo.data.Store`**: Managing collections of records.
13
+
14
+ ## What is a Record?
15
+
16
+ A Record in Neo.mjs is a dynamically generated, lightweight JavaScript object that represents a single row or item of data.
17
+ Crucially, Records do **not** extend `Neo.core.Base` or `Neo.data.Model`; they are plain objects with reactive properties.
18
+ This design choice makes them extremely performant and memory-efficient. When you modify a property of a Record, it automatically
19
+ triggers events, allowing UI components or other parts of your application to react to these changes.
20
+
21
+ **Benefits of using Records:**
22
+
23
+ - **Structured Data**: Records enforce a predefined structure based on a `Neo.data.Model`, ensuring data consistency.
24
+ - **Reactivity**: Changes to record fields are observable, simplifying UI updates and data synchronization.
25
+ - **Data Integrity**: Built-in type conversion and validation (defined in the Model) help maintain data quality.
26
+ - **Dirty Tracking**: Easily determine if a record or specific fields within it have been modified from their original state.
27
+ - **Integration with Stores**: Records are designed to work seamlessly with `Neo.data.Store` for managing collections of data.
28
+
29
+ ## `Neo.data.Model`: The Blueprint for Your Records
30
+
31
+ `Neo.data.Model` is the **central blueprint** for your Records. It defines the complete structure, data types, default
32
+ values, and any custom logic for data processing or validation. Every Record instance is an embodiment of its associated
33
+ Model. Each `Neo.data.Model` is a class that extends `Neo.core.Base`. Records, however, are instances of dynamically generated classes, not direct extensions of `Neo.core.Base`.
34
+
35
+ ### Key `Neo.data.Model` Configurations:
36
+
37
+ - **`fields`**: An array of objects, where each object defines a field of the record. This is where you specify the
38
+ data schema. Each field can have properties like:
39
+ - `name` (String, required): The unique identifier for the field within the record.
40
+ - `type` (String): The data type (e.g., `'string'`, `'number'`, `'boolean'`, `'date'`, `'int'`, `'float'`, `'html'`).
41
+ Neo.mjs provides automatic type conversion based on this type.
42
+ - `defaultValue` (Any): A value that will be assigned to the field if it's not provided when creating a record.
43
+ - `mapping` (String): A dot-separated string used to extract the field's value from a nested path within the raw
44
+ data received (e.g., `'address.street'` would map to `record.address.street`).
45
+ - `calculate` (Function): A powerful function that defines a **computed property**. The value of this field is
46
+ dynamically calculated based on other fields in the record. When the source fields change, the calculated field
47
+ automatically updates.
48
+ - `convert` (Function): A custom function to perform more complex data transformations or validations on the
49
+ field's value during assignment.
50
+ - `nullable` (Boolean): If `false`, the field cannot be `null`.
51
+ - `maxLength` (Number): Maximum length for string types. Values exceeding this may trigger a warning.
52
+ - `minLength` (Number): Minimum length for string types. Values falling below this may trigger a warning.
53
+ - **Nested Fields**: A field can itself contain a `fields` array, allowing you to define complex, hierarchical data
54
+ structures directly within your model (e.g., an `address` field with nested `street`, `city`, `zip` fields).
55
+ - **`keyProperty`**: (String, default: `'id'`) The field name that uniquely identifies each record within a
56
+ `Neo.data.Store`. This is crucial for efficient lookups and operations.
57
+ - **`trackModifiedFields`**: (Boolean, default: `false`) If `true`, the record will track changes to individual fields,
58
+ allowing you to determine which fields have been modified. **Be aware that enabling this will cause the record to store
59
+ a copy of its original data, effectively doubling the memory footprint for each record. Only enable this feature if you
60
+ specifically require granular dirty tracking.**
61
+
62
+ ### Dynamic Model Fields
63
+
64
+ While typically defined once, `Neo.data.Model` instances can have their `fields` configuration changed at runtime. If
65
+ the `fields` config of an already created `Model` instance is modified, `Neo.data.RecordFactory` will dynamically update
66
+ the associated Record class. This allows for advanced scenarios where your data schema might evolve during the application's
67
+ lifecycle.
68
+
69
+
70
+ ## `Neo.data.RecordFactory`: The Engine Behind Records
71
+
72
+ `Neo.data.RecordFactory` is a singleton class responsible for taking your `Neo.data.Model` definitions and dynamically
73
+ generating JavaScript classes for your Records. It intercepts property access on Record instances to provide reactivity,
74
+ type conversion, and dirty tracking.
75
+
76
+ When you create a new Record (typically via a `Neo.data.Store` or directly using `RecordFactory.createRecord()`), the
77
+ `RecordFactory`:
78
+
79
+ 1. Checks if a Record class for the given Model already exists. If not, it creates one. This dynamically generated class implicitly extends `Object`, making records as lightweight as possible and efficient.
80
+ 2. Defines getters and setters for each field specified in your Model. These getters and setters are what make Records
81
+ reactive.
82
+ 3. Applies default values and performs initial data parsing/conversion.
83
+ 4. Initializes dirty tracking if `trackModifiedFields` is enabled in the Model.
84
+
85
+ You generally won't interact directly with `RecordFactory` unless you're creating records outside of a `Store`.
86
+ In most real-world scenarios, when you add plain JavaScript objects to a `Neo.data.Store` or load JSON data from a backend into a `Store`, the `Store` automatically leverages `RecordFactory` to convert each item into a reactive Record. This means developers will very rarely need to use `RecordFactory` manually.
87
+
88
+ ### Example: Creating a Record Directly
89
+
90
+ ```javascript
91
+ import RecordFactory from '../../src/data/RecordFactory.mjs';
92
+ import UserModel from './UserModel.mjs'; // Assuming UserModel is defined as above
93
+
94
+ const userModelInstance = Neo.create(UserModel); // Create an instance of your Model
95
+
96
+ const userRecord = RecordFactory.createRecord(userModelInstance, {
97
+ age : 28,
98
+ email : 'jane.doe@example.com',
99
+ firstName: 'Jane',
100
+ id : 101,
101
+ lastName : 'Doe',
102
+
103
+ address: {
104
+ city : 'Anytown',
105
+ street: '123 Main St'
106
+ }
107
+ });
108
+
109
+ console.log(userRecord.fullName); // Output: Jane Doe (calculated field)
110
+ userRecord.age = '30'; // Automatic type conversion from string to int
111
+ console.log(userRecord.age); // Output: 30
112
+
113
+ // Accessing nested fields
114
+ // IMPORTANT: Direct access like `userRecord.address.street` will result in a JavaScript error
115
+ // because `userRecord.address` is undefined. Always use the full string path for nested fields.
116
+ console.log(userRecord['address.street']); // Output: 123 Main St
117
+ console.log(userRecord['address.city']); // Output: Anytown
118
+
119
+ // Modifying nested fields using string path
120
+ userRecord['address.street'] = '456 Oak Ave';
121
+ console.log(userRecord['address.street']); // Output: 456 Oak Ave
122
+
123
+ userRecord['address.city'] = 'Newville';
124
+ console.log(userRecord['address.city']); // Output: Newville
125
+
126
+ // Wrong way: Accessing the raw internal data (DO NOT USE FOR REACTIVE UPDATES).
127
+ // Direct modification of the data holder object will NOT trigger `recordChange` events or update dirty tracking.
128
+ // This gives you a REFERENCE to the internal data holder object.
129
+ const rawAddress = userRecord[Symbol.for('data')].address;
130
+ console.log(rawAddress.street); // Output: 456 Oak Ave
131
+
132
+ // Correct way (for a safe, disconnected copy):
133
+ // This gives you a STRUCTURED CLONE (a disconnected copy) of the data holder object.
134
+ const safeRawAddress = userRecord.toJSON().address;
135
+ console.log(safeRawAddress.street); // Output: 456 Oak Ave
136
+
137
+ // Modifying nested fields using set() with nested object structure
138
+ userRecord.set({ address: { street: '789 Pine Ln' } });
139
+ console.log(userRecord['address.street']); // Output: 789 Pine Ln
140
+ console.log(userRecord['address.city']); // Output: Newville (sibling untouched)
141
+ ```
142
+
143
+ ## Reactivity and Dirty Tracking
144
+
145
+ Records are inherently reactive. When you change a field's value, the setter defined by `RecordFactory` intercepts the
146
+ change, updates the internal data, and can trigger events. If the Model has `trackModifiedFields: true`, the Record also
147
+ keeps track of its original state.
148
+
149
+ - **`isModified`**: A boolean property on the Record instance that is `true` if any field has been changed from its
150
+ original value.
151
+ - **`isModifiedField(fieldName)`**: A method to check if a specific field has been modified.
152
+ - **`set(fields)`**: Bulk-update multiple fields and trigger a single change event. This method is particularly powerful
153
+ for nested objects: it performs a **deep merge** of the provided `fields` object with the record's existing data.
154
+ This means you can update specific properties within a nested object without overwriting the entire nested object.
155
+ For example, `myRecord.set({ address: { street: 'New Street' } })` will update only the `street` property within
156
+ `address`, leaving other `address` properties untouched. This contrasts with direct assignment to a nested object,
157
+ which would replace the entire nested object.
158
+ - **`setSilent(fields)`**: Bulk-update multiple fields without triggering a change event.
159
+ - **`toJSON()`**: A method available on every Record instance that returns a plain JavaScript object representing the
160
+ record's current data. Crucially, it returns a **structured clone** of the internal data. This ensures that any
161
+ modifications made to the object returned by `toJSON()` will **not** affect the original record and will **not**
162
+ trigger `recordChange` events, providing a safe, disconnected snapshot for serialization or external processing.
163
+
164
+ ### Bad Practice: Overwriting Nested Objects with Direct Assignment
165
+
166
+ While direct assignment to a nested *leaf property* using its full string path (e.g.,
167
+ `myRecord['address.street'] = "New Street";`) is reactive and works, directly assigning to a nested *object property*
168
+ (a non-leaf node) is generally considered a **bad practice** compared to using `record.set()` for several reasons:
169
+
170
+ 1. **Complete Overwrite**: If you assign directly to a nested object property (e.g., `myRecord.address = { newProp: 'value' };`),
171
+ you will **completely overwrite** the existing nested object. Any other properties within that nested object that are
172
+ not explicitly included in your new assignment will be **lost**. `record.set()` performs a **deep merge**, intelligently
173
+ updating only the specified nested properties while preserving others.
174
+ 2. **Multiple Change Events (for multiple field updates)**: If you need to update several fields (even leaf properties,
175
+ nested or not), performing multiple direct assignments will trigger a separate `recordChange` event for each assignment.
176
+ `record.set()` allows you to batch all these updates into a single operation, triggering only one `recordChange` event,
177
+ which is significantly more efficient for UI updates and overall application performance.
178
+ 3. **Clarity and Consistency**: Using `record.set()` is the idiomatic and recommended way to modify record data,
179
+ especially for nested structures. It clearly communicates intent and promotes consistent API usage across your application.
180
+
181
+ Always prefer `record.set()` for modifying record data, particularly when dealing with nested fields or multiple updates,
182
+ to leverage its deep merge capabilities and optimize event triggering.
183
+
184
+ ### Example: Reactivity and Dirty Tracking
185
+
186
+ ```javascript
187
+ import RecordFactory from '../../src/data/RecordFactory.mjs';
188
+ import UserModel from './UserModel.mjs';
189
+
190
+ const userModelInstance = Neo.create(UserModel);
191
+ const userRecord = RecordFactory.createRecord(userModelInstance, {
192
+ email : 'john.smith@example.com',
193
+ firstName: 'John',
194
+ id : 102,
195
+ lastName : 'Smith',
196
+
197
+ address: {
198
+ city : 'Oldtown',
199
+ street: '100 Elm St'
200
+ }
201
+ });
202
+
203
+ console.log(userRecord.isModified); // Output: false
204
+
205
+ userRecord.firstName = 'Jonathan';
206
+ console.log(userRecord.isModified); // Output: true
207
+ console.log(userRecord.isModifiedField('firstName')); // Output: true
208
+ console.log(userRecord.isModifiedField('lastName')); // Output: false
209
+
210
+ userRecord.set({ address: { city: 'Newtown' } }); // Update nested field using set()
211
+ console.log(userRecord.isModifiedField('address.city')); // Output: true
212
+
213
+ userRecord.reset({firstName: 'John'}); // Reset firstName to original
214
+ console.log(userRecord.isModified); // Output: true (because address.city is still modified)
215
+
216
+ userRecord.reset(); // Reset all fields to original state
217
+ console.log(userRecord.isModified); // Output: false
218
+ ```
219
+
220
+ ## Interaction with `Neo.data.Store`
221
+
222
+ `Neo.data.Store` is designed to manage collections of Records. When you add raw data (plain JavaScript objects) to a
223
+ `Store`, it automatically uses its associated `Neo.data.Model` and `RecordFactory` to convert them into reactive Record
224
+ instances.
225
+
226
+ - **`store.add(data)`**: Converts data into Records and adds them to the store.
227
+ - **`store.model`**: The `Neo.data.Model` instance associated with the store, defining the structure of its records.
228
+ - **`recordChange` event**: Stores emit a `recordChange` event when a field of one of its records is modified. This
229
+ allows UI components (like Grids) to efficiently update only the changed cells. For a real-world example, see how
230
+ `Neo.grid.Body`'s `onStoreRecordChange` method consumes this event to perform targeted cell updates.
231
+
232
+ ### Example: Store Managing Records
233
+
234
+ ```javascript
235
+ import Store from '../../src/data/Store.mjs';
236
+ import UserModel from './UserModel.mjs';
237
+
238
+ const userStore = Neo.create(Store, {
239
+ model: UserModel, // Link the store to your UserModel
240
+ data: [
241
+ {id: 201, firstName: 'Anna', lastName: 'Brown', email: 'anna.b@example.com'},
242
+ {id: 202, firstName: 'Peter', lastName: 'Green', email: 'peter.g@example.com'}
243
+ ]
244
+ });
245
+
246
+ userStore.on('recordChange', ({record, fields}) => {
247
+ console.log(`Record ${record.id} changed:`, fields);
248
+ });
249
+
250
+ const anna = userStore.get(201);
251
+ anna.email = 'anna.brown@example.com';
252
+ // Output: Record 201 changed: [{name: "email", oldValue: "anna.b@example.com", value: "anna.brown@example.com"}]
253
+
254
+ console.log(userStore.get(201).isModified); // Output: true
255
+ ```
256
+
257
+ ## The Reactivity Masterpiece: Collections and Records in Harmony
258
+
259
+ The true power of Neo.mjs's data layer emerges when `Neo.collection.Base` (used by `Neo.data.Store`) and `Neo.data.Model`
260
+ (Records) work together. This combination creates a highly reactive and efficient system for managing structured, often
261
+ tabular, application data.
262
+
263
+ `Neo.data.Store` acts as a specialized `Neo.collection.Base` that manages a collection of `Neo.data.Model` instances
264
+ (Records). This means you benefit from two layers of reactivity:
265
+
266
+ 1. **Collection-Level Reactivity**:
267
+ * When records are added to, removed from, or reordered within a `Store`, the `Store` (as a `Neo.collection.Base`)
268
+ fires `mutate` events. This allows UI components to react to structural changes in the dataset (e.g., a new row
269
+ appearing in a grid, or a row being deleted).
270
+
271
+ 2. **Record-Level Reactivity**:
272
+ * When a field *within an individual Record* changes its value (e.g., `myUserRecord.firstName = 'New Name'`),
273
+ the Record itself (via the `RecordFactory`'s generated setters) notifies its owning `Store` (if the Model has a
274
+ `storeId` pointing back to the Store). This triggers the `recordChange` event on the `Store`. This allows UI
275
+ components to react to granular changes within a data item (e.g., updating a single cell in a grid without
276
+ re-rendering the entire row or grid).
277
+
278
+ This dual-layered reactivity is a cornerstone of Neo.mjs's performance. It enables highly optimized UI updates, as
279
+ components can precisely react to only the changes that affect them, avoiding costly full re-renders.
280
+
281
+ ## Conclusion
282
+
283
+ Records, powered by `Neo.data.Model` and `Neo.data.RecordFactory`, are a cornerstone of data management in Neo.mjs.
284
+ They provide a robust, reactive, and structured approach to handling application data, simplifying complex tasks like UI
285
+ synchronization, data validation, and state tracking. By leveraging Records, you can build more maintainable, performant,
286
+ and predictable data-driven applications.
@@ -15,7 +15,7 @@ While 99% of Neo.mjs development happens at the Component Tree layer, creating c
15
15
  Neo.mjs VDom nodes are plain JavaScript objects that represent DOM elements.
16
16
  **Important**: VDom only contains structure, styling, content, and attributes - **never event listeners**.
17
17
 
18
- ```javascript
18
+ ```javascript readonly
19
19
  // Basic VDom node structure
20
20
  {
21
21
  tag : 'div', // HTML tag (default: 'div')
@@ -41,7 +41,7 @@ Neo.mjs VDom nodes are plain JavaScript objects that represent DOM elements.
41
41
 
42
42
  Components define their internal DOM structure via the `vdom` config:
43
43
 
44
- ```javascript
44
+ ```javascript readonly
45
45
  import Component from './src/component/Base.mjs';
46
46
 
47
47
  class CustomButton extends Component {
@@ -81,7 +81,7 @@ For a comprehensive deep dive into all aspects of DOM event handling in Neo.mjs
81
81
 
82
82
  Here's a simple example of how an event handler defined via `domListeners` would interact with a component's VDom:
83
83
 
84
- ```javascript
84
+ ```javascript readonly
85
85
  import Component from './src/component/Base.mjs';
86
86
  import VdomUtil from './src/util/Vdom.mjs'; // For accessing VDom nodes by flag
87
87
 
@@ -144,7 +144,7 @@ class InteractiveComponent extends Component {
144
144
 
145
145
  The typical way to sync VDom changes to the DOM is through the component's `update()` method:
146
146
 
147
- ```javascript
147
+ ```javascript readonly
148
148
  import Component from './src/component/Base.mjs'; // Required import
149
149
 
150
150
  class StandardComponent extends Component {
@@ -180,7 +180,7 @@ class StandardComponent extends Component {
180
180
  For performance-critical scenarios, you can bypass the VDom worker's diffing engine and send manually crafted deltas
181
181
  directly from the App Worker to the Main Thread. This offers precise control but requires careful manual delta construction.
182
182
 
183
- ```javascript
183
+ ```javascript readonly
184
184
  import Component from './src/component/Base.mjs'; // Required import
185
185
 
186
186
  class AdvancedComponent extends Component {
@@ -240,7 +240,7 @@ class AdvancedComponent extends Component {
240
240
 
241
241
  Flags provide efficient, direct access to specific VDom nodes within a component's `vdom` structure, avoiding the need for DOM queries.
242
242
 
243
- ```javascript
243
+ ```javascript readonly
244
244
  import Component from './src/component/Base.mjs';
245
245
  import VdomUtil from './src/util/Vdom.mjs'; // Required import for VdomUtil
246
246
  import NeoArray from './src/util/Array.mjs'; // Required import for NeoArray
@@ -304,7 +304,7 @@ class IconButton extends Component {
304
304
 
305
305
  Build VDom structures programmatically, often in response to data changes. This is common for lists or complex, data-driven UI fragments.
306
306
 
307
- ```javascript
307
+ ```javascript readonly
308
308
  import Component from './src/component/Base.mjs'; // Required import
309
309
 
310
310
  class DataList extends Component {
@@ -380,7 +380,7 @@ class DataList extends Component {
380
380
 
381
381
  For sophisticated UI patterns like 3D visualizations or complex dynamic layouts, you might imperatively calculate and apply VDom properties or even use `Neo.applyDeltas()` for maximum performance.
382
382
 
383
- ```javascript
383
+ ```javascript readonly
384
384
  import Component from './src/component/Base.mjs'; // Base component class
385
385
 
386
386
  class Helix extends Component {
@@ -452,7 +452,7 @@ class Helix extends Component {
452
452
 
453
453
  ### XSS Prevention
454
454
 
455
- ```javascript
455
+ ```javascript readonly
456
456
  import Component from './src/component/Base.mjs'; // Required import
457
457
  // import DOMPurify from 'dompurify'; // Example for external sanitization library
458
458
 
@@ -499,7 +499,7 @@ this.update();
499
499
 
500
500
  ### 1. Batch VDom Updates
501
501
 
502
- ```javascript
502
+ ```javascript readonly
503
503
  import Component from './src/component/Base.mjs'; // Required import
504
504
  import Neo from './src/Neo.mjs'; // Required import for Neo.applyDeltas
505
505
 
@@ -547,7 +547,7 @@ class PerformantComponent extends Component {
547
547
 
548
548
  ### 2. Efficient Event Delegation
549
549
 
550
- ```javascript
550
+ ```javascript readonly
551
551
  import Component from './src/component/Base.mjs'; // Required import
552
552
 
553
553
  class EfficientEventComponent extends Component {
@@ -595,7 +595,7 @@ super.construct(config);
595
595
 
596
596
  ### 3. Memory Management
597
597
 
598
- ```javascript
598
+ ```javascript readonly
599
599
  import Component from './src/component/Base.mjs'; // Required import
600
600
 
601
601
  class MemoryEfficientComponent extends Component {
@@ -632,7 +632,7 @@ class MemoryEfficientComponent extends Component {
632
632
 
633
633
  Dynamically show or hide VDom nodes by setting their `removeDom` property. This is efficient as the VDom node remains in the tree, but its corresponding DOM element is removed/added from the document flow by the framework.
634
634
 
635
- ```javascript
635
+ ```javascript readonly
636
636
  import Component from './src/component/Base.mjs'; // Required import
637
637
  import VdomUtil from './src/util/Vdom.mjs'; // Required import
638
638
 
@@ -673,7 +673,7 @@ class ConditionalComponent extends Component {
673
673
 
674
674
  Programmatically create and update lists of VDom nodes, typically from data. This approach is highly efficient as the VDom diffing engine optimizes the DOM updates.
675
675
 
676
- ```javascript
676
+ ```javascript readonly
677
677
  import Component from './src/component/Base.mjs'; // Required import
678
678
 
679
679
  class ListComponent extends Component {
@@ -0,0 +1,241 @@
1
+ # ComboBox Field
2
+
3
+ The `Neo.form.field.ComboBox` is a powerful and flexible input component that provides a dropdown list for selecting one or multiple items. It combines the functionality of a text input field with a list, allowing users to either type to filter options or select directly from a predefined set of choices.
4
+
5
+ ## 1. Basic Usage
6
+
7
+ At its simplest, a `ComboBox` can be configured with a static array of data.
8
+
9
+ ```javascript live-preview
10
+ import ComboBox from '../../src/form/field/ComboBox.mjs';
11
+ import FormContainer from '../../src/form/Container.mjs';
12
+
13
+ class MainView extends FormContainer {
14
+ static config = {
15
+ className: 'MyComboBoxForm',
16
+ layout : {ntype: 'vbox', align: 'start'},
17
+ items : [{
18
+ module : ComboBox,
19
+ labelPosition: 'inline',
20
+ labelText : 'Select a Fruit',
21
+ name : 'selectedFruit',
22
+ width : 200,
23
+
24
+ store: { // Inline store configuration
25
+ data: [
26
+ {id: 'apple', name: 'Apple'},
27
+ {id: 'banana', name: 'Banana'},
28
+ {id: 'orange', name: 'Orange'},
29
+ {id: 'grape', name: 'Grape'}
30
+ ]
31
+ }
32
+ }]
33
+ }
34
+ }
35
+
36
+ MainView = Neo.setupClass(MainView);
37
+ ```
38
+
39
+ In this example:
40
+ * The `store` config is an inline object that gets automatically converted into a `Neo.data.Store`.
41
+ * The `id` property of each data item is used as the internal value, and `name` is used for display by default.
42
+ * **Note**: If your data uses different property names for the unique identifier or display value (e.g., `cityId` instead of `id`), you must explicitly define a `model` (even inline) within the `store` config and set the `keyProperty` to match your unique identifier.
43
+
44
+ ## 2. Data Integration with `Neo.data.Store`
45
+
46
+ For more complex scenarios, especially when dealing with large datasets or remote data, it's best practice to use a separate `Neo.data.Store` instance.
47
+
48
+ ```javascript readonly
49
+ import ComboBox from '../../src/form/field/ComboBox.mjs';
50
+ import Store from '../../src/data/Store.mjs';
51
+ import Model from '../../src/data/Model.mjs';
52
+
53
+ // Define a Model for your data
54
+ class CountryModel extends Model {
55
+ static config = {
56
+ className: 'CountryModel',
57
+ fields: [
58
+ {name: 'id', type: 'String'},
59
+ {name: 'name', type: 'String'}
60
+ ]
61
+ }
62
+ }
63
+
64
+ // Define a Store class
65
+ class CountriesStore extends Store {
66
+ static config = {
67
+ className: 'CountriesStore',
68
+ model : CountryModel,
69
+ autoLoad : true,
70
+ url : '/path/to/your/countries.json' // Example: fetch data from a URL
71
+ }
72
+ }
73
+
74
+ class CountryComboBoxForm extends Neo.form.Container {
75
+ static config = {
76
+ className: 'CountryComboBoxForm',
77
+ layout : {ntype: 'vbox', align: 'start'},
78
+ items : [{
79
+ module : ComboBox,
80
+ labelText: 'Select a Country',
81
+ name : 'selectedCountry',
82
+ store : CountriesStore // Pass the Store Class
83
+ }]
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### Passing Models and Stores: Flexibility in Configuration
89
+
90
+ Neo.mjs offers significant flexibility in how you configure models and stores for your components. For both the `model` config within a `Store` and the `store` config within a data-bound component (like `ComboBox`), you can typically pass one of three types:
91
+
92
+ 1. **Configuration Object**: A plain JavaScript object containing the properties for the model or store. Neo.mjs will automatically create an instance from this object. This is convenient for inline, simple definitions.
93
+ ```javascript readonly
94
+ // Example: Inline Store config for ComboBox
95
+ store: {
96
+ model: { // Model config object
97
+ fields: [{name: 'id'}, {name: 'name'}]
98
+ },
99
+ data: [{id: 1, name: 'Item 1'}]
100
+ }
101
+ ```
102
+
103
+ 2. **Class Reference**: A direct reference to the class (e.g., `MyStoreClass`, `MyModelClass`). Neo.mjs will automatically instantiate this class when the component or store is created. This is the most common and recommended approach for reusable definitions.
104
+ ```javascript readonly
105
+ // Example: Passing a Store Class to ComboBox
106
+ import MyStoreClass from './MyStoreClass.mjs';
107
+
108
+ // ...
109
+ items: [{
110
+ module: ComboBox,
111
+ store : MyStoreClass // Pass the Store Class
112
+ }]
113
+ ```
114
+ ```javascript readonly
115
+ // Example: Passing a Model Class to a Store
116
+ import MyModelClass from './MyModelClass.mjs';
117
+
118
+ class MyStoreClass extends Neo.data.Store {
119
+ static config = {
120
+ model: MyModelClass // Pass the Model Class
121
+ }
122
+ }
123
+ ```
124
+
125
+ 3. **Instance**: A pre-created instance of the model or store. This is useful when you need a single, shared instance across multiple components (e.g., a singleton store for application-wide settings or a store that's managed externally).
126
+ ```javascript readonly
127
+ // Example: Passing a Store Instance to ComboBox
128
+ const mySharedStore = Neo.create({
129
+ module: Neo.data.Store,
130
+ // ... store configs
131
+ });
132
+
133
+ // ...
134
+ items: [{
135
+ module: ComboBox,
136
+ store : mySharedStore // Pass the Store Instance
137
+ }]
138
+ ```
139
+ While less common for `model` configs (as models are typically instantiated by stores), you could theoretically pass a `Model` instance if a custom scenario required it.
140
+
141
+ Choosing the appropriate method depends on your application's architecture, reusability needs, and whether you require shared or independent data instances.
142
+
143
+ ## 3. Key Configuration Options
144
+
145
+ The `ComboBox` offers several configurations to control its behavior and appearance:
146
+
147
+ * **`displayField`**: (Default: `'name'`) The name of the field in the store's records that will be displayed in the dropdown list and the input field.
148
+ * **`valueField`**: (Default: `'id'`) The name of the field in the store's records that will be used as the actual value of the `ComboBox` when selected. This is the value returned by `getSubmitValue()`.
149
+ * **`forceSelection`**: (Default: `true`) If `true`, the `ComboBox` will only allow values that match an existing record in its store. If the user types a value that doesn't match, the field will be cleared on blur.
150
+ * **`editable`**: (Default: `true`) If `true`, the user can type into the input field. If `false`, the input field is read-only, and selection can only be made from the dropdown list.
151
+ * **`filterDelay`**: (Default: `50`) The time in milliseconds to delay between user input and applying the filter to the dropdown list. Useful for performance with large stores.
152
+ * **`typeAhead`**: (Default: `true`) If `true`, as the user types, the `ComboBox` will attempt to complete the input with the first matching record from the store, displaying the suggestion as a hint.
153
+ * **`triggerAction`**: (Default: `'all'`) Controls what happens when the dropdown trigger is clicked.
154
+ * `'all'`: Shows all list items, regardless of current input.
155
+ * `'filtered'`: Shows only items that match the current input field's value.
156
+ * **`listConfig`**: An object that allows you to configure the underlying `Neo.list.Base` component used for the dropdown. For example, you can enable checkboxes for multi-selection (though `ComboBox` itself is typically single-select, `Chip` extends it for multi-select).
157
+
158
+ ```javascript live-preview
159
+ import ComboBox from '../../src/form/field/ComboBox.mjs';
160
+ import FormContainer from '../../src/form/Container.mjs';
161
+
162
+ class MainView extends FormContainer {
163
+ static config = {
164
+ className: 'AdvancedComboBox',
165
+ layout : {ntype: 'vbox', align: 'start'},
166
+ items : [{
167
+ module : ComboBox,
168
+ displayField : 'cityName',
169
+ editable : true,
170
+ filterDelay : 200,
171
+ forceSelection: false, // Allow custom input
172
+ labelText : 'Search for a City',
173
+ name : 'citySearch',
174
+ triggerAction : 'filtered',
175
+ typeAhead : true,
176
+ valueField : 'cityId',
177
+ width : 300,
178
+
179
+ store: {
180
+ keyProperty: 'cityId',
181
+ model : {fields: [{name: 'cityId', type: 'String'}, {name: 'cityName', type: 'String'}]},
182
+
183
+ data: [
184
+ {cityId: 'ny', cityName: 'New York'},
185
+ {cityId: 'la', cityName: 'Los Angeles'},
186
+ {cityId: 'chi', cityName: 'Chicago'},
187
+ {cityId: 'hou', cityName: 'Houston'}
188
+ ]
189
+ }
190
+ }]
191
+ }
192
+ }
193
+
194
+ MainView = Neo.setupClass(MainView);
195
+ ```
196
+
197
+ ## 4. Events
198
+
199
+ The `ComboBox` field emits several events that you can listen to for custom logic:
200
+
201
+ * **`change`**: Inherited from `Neo.form.field.Base`, fired when the field's `value` config changes.
202
+ * **`userChange`**: Inherited from `Neo.form.field.Base`, fired when the field's value changes due to direct user interaction (e.g., typing or selecting from the list).
203
+ * **`select`**: Specific to `ComboBox`, fired when an item is selected from the dropdown list. The event object contains the `record` that was selected.
204
+
205
+ ```javascript live-preview
206
+ import ComboBox from '../../src/form/field/ComboBox.mjs';
207
+ import FormContainer from '../../src/form/Container.mjs';
208
+
209
+ class MainView extends FormContainer {
210
+ static config = {
211
+ className: 'ComboBoxWithEvents',
212
+ layout : {ntype: 'vbox', align: 'start'},
213
+ items : [{
214
+ module : ComboBox,
215
+ labelPosition: 'top',
216
+ labelText : 'Choose an Option',
217
+ name : 'option',
218
+
219
+ store: {
220
+ data: [
221
+ {id: 1, name: 'Option A'},
222
+ {id: 2, name: 'Option B'}
223
+ ]
224
+ },
225
+ listeners: {
226
+ select: function(data) {
227
+ Neo.Main.log({value: `Selected record: ${data.value.name}`}); // data.value is the selected record
228
+ },
229
+ change: function(data) {
230
+ Neo.Main.log({value: `Field value changed (record or null): ${data.value?.name || data.value}`});
231
+ },
232
+ userChange: function(data) { // text input
233
+ Neo.Main.log({value: `User interacted, new value: ${data.value}`});
234
+ }
235
+ }
236
+ }]
237
+ }
238
+ }
239
+
240
+ MainView = Neo.setupClass(MainView);
241
+ ```