neo.mjs 10.4.0 → 10.5.0

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 (38) hide show
  1. package/.github/RELEASE_NOTES/v10.4.1.md +16 -0
  2. package/.github/RELEASE_NOTES/v10.5.0.md +51 -0
  3. package/ServiceWorker.mjs +2 -2
  4. package/apps/finance/view/ViewportController.mjs +1 -1
  5. package/apps/form/view/SideNavList.mjs +1 -1
  6. package/apps/portal/index.html +1 -1
  7. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  8. package/apps/realworld2/view/article/PreviewList.mjs +1 -1
  9. package/docs/app/view/classdetails/MainContainerController.mjs +1 -1
  10. package/docs/app/view/classdetails/MembersList.mjs +5 -5
  11. package/examples/grid/bigData/ControlsContainer.mjs +6 -6
  12. package/examples/grid/bigData/MainStore.mjs +4 -4
  13. package/examples/grid/cellEditing/MainContainerStateProvider.mjs +1 -1
  14. package/examples/grid/nestedRecordFields/EditUserDialog.mjs +1 -1
  15. package/examples/grid/nestedRecordFields/ViewportController.mjs +1 -1
  16. package/examples/table/cellEditing/MainContainerStateProvider.mjs +1 -1
  17. package/examples/table/nestedRecordFields/EditUserDialog.mjs +1 -1
  18. package/examples/table/nestedRecordFields/ViewportStateProvider.mjs +1 -1
  19. package/examples/tableStore/MainContainer.mjs +2 -2
  20. package/package.json +1 -1
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/collection/Base.mjs +24 -6
  23. package/src/component/Gallery.mjs +3 -3
  24. package/src/component/Helix.mjs +4 -4
  25. package/src/component/wrapper/GoogleMaps.mjs +1 -1
  26. package/src/data/Store.mjs +105 -12
  27. package/src/draggable/list/DragZone.mjs +1 -1
  28. package/src/draggable/tree/DragZone.mjs +1 -1
  29. package/src/form/field/ComboBox.mjs +10 -2
  30. package/src/grid/Body.mjs +99 -14
  31. package/src/grid/Container.mjs +13 -8
  32. package/src/grid/VerticalScrollbar.mjs +7 -6
  33. package/src/grid/column/Component.mjs +7 -2
  34. package/src/list/Base.mjs +1 -1
  35. package/src/selection/TreeAccordionModel.mjs +9 -3
  36. package/src/table/Body.mjs +29 -2
  37. package/src/tree/Accordion.mjs +2 -2
  38. package/src/tree/List.mjs +3 -3
@@ -0,0 +1,16 @@
1
+ # Neo.mjs v10.4.1 Release Notes
2
+
3
+ ## Highlights
4
+
5
+ This patch release polishes the "instant preview" feature for grids introduced in v10.4.0, ensuring the grid's scrollbar and accessibility properties are correctly sized from the very first paint when loading large datasets. The non-buffered `table.Body` also receives a major performance boost for clearing data.
6
+
7
+ ## Enhancements & Bug Fixes
8
+
9
+ ### 1. Polished "Instant Preview" for Large Datasets in Grids
10
+ The chunked loading mechanism for stores and grids has been refined. When adding a large dataset, the initial `load` event now carries the final total record count. The `grid.Container` uses this information immediately to set the correct `aria-rowcount` and to size the vertical scrollbar accurately, preventing layout shifts and providing a more stable and accessible user experience from the very first render.
11
+
12
+ ### 2. Standardized Internal Store `load` Event
13
+ To support the "instant preview" refinements, the `data.Store` `load` event payload has been standardized internally. It now consistently uses an object containing an `items` property (e.g., `{items: [...]}`). Framework components have been updated to use this new signature.
14
+
15
+ ### 3. Performance Fast Path for `table.Body`
16
+ The non-buffered `table.Body` is now significantly faster when clearing its data. It uses the same "fast path" optimization as `grid.Body`, directly clearing the DOM instead of processing a large and unnecessary VDOM diff when an empty dataset is loaded. This aligns the performance of the two components.
@@ -0,0 +1,51 @@
1
+ # Neo.mjs v10.5.0 Release Notes
2
+
3
+ ## Major Performance Enhancements for Grids and Data Handling
4
+
5
+ This release introduces a groundbreaking set of performance optimizations for `Neo.data.Store` and `Neo.grid.Container`, fundamentally transforming how large datasets are handled within Neo.mjs applications. These enhancements dramatically reduce initial load times, improve UI responsiveness, and resolve critical VDom reconciliation issues, making it feasible to work with millions of data points with unprecedented fluidity.
6
+
7
+ ### Key Highlights:
8
+
9
+ * **Lazy Record Instantiation (GET-driven approach):**
10
+ * `Neo.data.Store` now defers the creation of `Neo.data.Record` instances. Raw data objects are stored directly, and `Record` instances are only created on-demand when an item is explicitly accessed (e.g., via `store.get()`, `store.getAt()`, or during VDom rendering of visible rows).
11
+ * **Impact:** This eliminates the massive upfront cost of instantiating millions of `Record` objects, leading to **up to 97% reduction in initial data processing time** for large datasets.
12
+
13
+ * **Configurable Data Chunking for UI Responsiveness:**
14
+ * Introduced an `initialChunkSize` config in `Neo.data.Store` (default `0`). When enabled, `Store.add()` processes data in chunks, significantly mitigating UI freezes during synchronous loading of extremely large datasets (e.g., 1,000,000+ rows).
15
+ * **Impact:** Provides a smoother perceived user experience during initial data loads, even for datasets that would otherwise cause multi-second UI blocks.
16
+
17
+ * **Robust Component ID Management for VDom Stability:**
18
+ * Resolved critical VDom reconciliation errors (e.g., `RangeError`, infinite loops) that could occur with component columns when chunking was active. `Neo.grid.column.Component` now intelligently generates unique IDs based on the store's chunking state, ensuring VDom stability.
19
+ * **Impact:** Eliminates a major source of instability and crashes when using component columns with large, dynamically loaded datasets.
20
+
21
+ * **Automated Component Instance Cleanup:**
22
+ * Enhanced `Neo.grid.Body` with sophisticated component instance management. Components are now automatically destroyed when the grid body is destroyed, the store is cleared, the store changes, or when they scroll out of view after a chunked load.
23
+ * **Impact:** Reduces memory overhead and improves long-term performance by preventing the accumulation of unused component instances.
24
+
25
+ * **GPU-Accelerated Vertical Scrolling (`translate3d`):**
26
+ * Grid rows now utilize `transform: translate3d(0px, Ypx, 0px)` for vertical positioning. This hints to the browser to promote rows to their own composite layers, offloading rendering to the GPU.
27
+ * **Impact:** Leads to noticeably smoother and more fluid vertical scrolling, especially during rapid movements through large grids.
28
+
29
+ ### Other Enhancements:
30
+
31
+ * **ComboBox Initial Display Fix:** Resolved an issue where `Neo.form.field.ComboBox` instances would appear blank on initial load when backed by lazily instantiated stores. The `updateInputValueFromValue` method now correctly displays values from raw data or `Record` instances.
32
+ * **Enhanced BigData Grid Example:** The `examples/grid/bigData` demo has been updated to include control options for up to 200 columns, allowing for testing with up to 20,000,000 cells. This provides a more extreme and comprehensive benchmark for grid performance.
33
+
34
+ These collective improvements mark a significant leap forward in Neo.mjs's capability to handle and display massive amounts of data with high performance and a superior user experience.
35
+
36
+ ---
37
+
38
+ ### Performance Benchmarks (from `examples/grid/bigData`):
39
+
40
+ | Scenario | Before (Eager Instantiation) | After (Lazy Instantiation, with chunking) | After (Lazy Instantiation, no chunking) |
41
+ | :------------------------------------- | :--------------------------- | :---------------------------------------- | :-------------------------------------- |
42
+ | 1,000 rows, 50 columns | 49ms (Record creation) | 2ms (Data gen + add) | 2ms (Data gen + add) |
43
+ | 50,000 rows, 50 columns | 2391ms (Record creation) | 1252ms (Data gen + add) | 93ms (Data gen + add) |
44
+ | 50,000 rows, 100 columns | 4377ms (Record creation) | 1289ms (Data gen + add) | 95ms (Data gen + add) |
45
+ | 100,000 rows, 50 columns | N/A (too slow) | 1299ms (Data gen + add) | 156ms (Data gen + add) |
46
+ | 100,000 rows, 200 columns | N/A (too slow) | 1427ms (Data gen + add) | 174ms (Data gen + add) |
47
+
48
+ *Note: "Record creation" refers to the time taken for `Neo.data.Record` instantiation. "Data gen + add" refers to the time taken to generate raw data and add it to the store's collection.*
49
+
50
+ **Important Note on Chunking:**
51
+ The introduction of lazy record instantiation significantly reduces the need for chunking in most common use cases, as the initial data loading is now extremely fast even for large datasets. However, Neo.mjs still optionally supports data chunking via the `initialChunkSize` config for scenarios involving truly massive synchronous data additions where mitigating UI freezes is paramount.
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.4.0'
23
+ * @member {String} version='10.5.0'
24
24
  */
25
- version: '10.4.0'
25
+ version: '10.5.0'
26
26
  }
27
27
 
28
28
  /**
@@ -35,7 +35,7 @@ class ViewportController extends Controller {
35
35
  companiesStore = me.getStore('companies'),
36
36
  items = [];
37
37
 
38
- companiesStore.items.forEach(record => {
38
+ companiesStore.forEach(record => {
39
39
  items.push({
40
40
  symbol: record.symbol,
41
41
  value : Math.random() * 1000
@@ -84,7 +84,7 @@ class SideNavList extends List {
84
84
  onStoreLoad() {
85
85
  let maxIndex = -1;
86
86
 
87
- this.store.items.forEach(record => {
87
+ this.store.forEach(record => {
88
88
  if (!record.isHeader) {
89
89
  maxIndex++
90
90
  }
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-08-11",
19
+ "datePublished": "2025-08-12",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.4.0'
111
+ text : 'v10.5.0'
112
112
  }]
113
113
  }],
114
114
  /**
@@ -33,7 +33,7 @@ class PreviewList extends List {
33
33
 
34
34
  me.vdom.cn = [];
35
35
 
36
- me.store.items.forEach(item => {
36
+ me.store.forEach(item => {
37
37
  listItem = Neo.create({
38
38
  module : PreviewComponent,
39
39
  parentId: me.id,
@@ -37,7 +37,7 @@ class MainContainerController extends Component {
37
37
  countProtecteds = 0,
38
38
  countStatics = 0;
39
39
 
40
- store.items.forEach(item => {
40
+ store.forEach(item => {
41
41
  if (item.kind === 'function') {
42
42
  countMethods++
43
43
  } else if (item.kind === 'member') {
@@ -128,7 +128,7 @@ class MembersList extends Base {
128
128
  * @returns {Object} vdom
129
129
  */
130
130
  applyConfigsHeader(store, vdom) {
131
- if (store.items[0]?.kind === 'member') {
131
+ if (store.getAt(0)?.kind === 'member') {
132
132
  vdom.cn.push({
133
133
  // scrolling placeholder
134
134
  }, {
@@ -152,7 +152,7 @@ class MembersList extends Base {
152
152
  applyEventsHeader(item, index, store, vdom) {
153
153
  if (
154
154
  item.kind === 'event' &&
155
- store.items[index -1]?.kind !== 'event'
155
+ store.getAt(index -1)?.kind !== 'event'
156
156
  ) {
157
157
  vdom.cn.push({
158
158
  // scrolling placeholder
@@ -179,8 +179,8 @@ class MembersList extends Base {
179
179
  if (
180
180
  item.kind === 'function' &&
181
181
  (
182
- !store.items[index -1] || (
183
- store.items[index -1]?.kind !== 'function'
182
+ !store.getAt(index -1) || (
183
+ store.getAt(index -1)?.kind !== 'function'
184
184
  )
185
185
  )
186
186
  ) {
@@ -211,7 +211,7 @@ class MembersList extends Base {
211
211
  vdom.cn = [];
212
212
  vdom = me.applyConfigsHeader(me.store, vdom);
213
213
 
214
- me.store.items.forEach((item, index) => {
214
+ me.store.forEach((item, index) => {
215
215
  vdom = me.applyEventsHeader( item, index, me.store, vdom);
216
216
  vdom = me.applyMethodsHeader(item, index, me.store, vdom);
217
217
 
@@ -17,10 +17,10 @@ class ControlsContainer extends Container {
17
17
  * @static
18
18
  */
19
19
  static delayable = {
20
- onAmountColumnsChange : {type: 'buffer', timer: 30},
21
- onAmountRowsChange : {type: 'buffer', timer: 30},
22
- onBufferColumnRangeChange: {type: 'buffer', timer: 30},
23
- onBufferRowRangeChange : {type: 'buffer', timer: 30}
20
+ onAmountColumnsChange : {type: 'buffer', timer: 15},
21
+ onAmountRowsChange : {type: 'buffer', timer: 15},
22
+ onBufferColumnRangeChange: {type: 'buffer', timer: 15},
23
+ onBufferRowRangeChange : {type: 'buffer', timer: 15}
24
24
  }
25
25
 
26
26
  static config = {
@@ -69,14 +69,14 @@ class ControlsContainer extends Container {
69
69
  labelText : 'Amount Rows',
70
70
  labelWidth: 120,
71
71
  listeners : {change: 'up.onAmountRowsChange'},
72
- store : ['1000', '5000', '10000', '20000', '50000'],
72
+ store : ['1000', '5000', '10000', '20000', '50000', '100000'],
73
73
  value : '1000',
74
74
  width : 200
75
75
  }, {
76
76
  labelText : 'Amount Columns',
77
77
  labelWidth: 145,
78
78
  listeners : {change: 'up.onAmountColumnsChange'},
79
- store : ['10', '25', '50', '75', '100'],
79
+ store : ['10', '25', '50', '75', '100', '200'],
80
80
  value : '50',
81
81
  width : 200
82
82
  }, {
@@ -74,7 +74,7 @@ class MainStore extends Store {
74
74
 
75
75
  me.model.amountColumns = value;
76
76
 
77
- console.log('Start creating records');
77
+ console.log('Start generating data and adding to collection');
78
78
 
79
79
  if (me.items?.length > 0) {
80
80
  me.clear()
@@ -82,7 +82,7 @@ class MainStore extends Store {
82
82
 
83
83
  me.add(data);
84
84
 
85
- console.log(`Record creation total time: ${Math.round(performance.now() - start)}ms`)
85
+ console.log(`Data generation and collection add total time: ${Math.round(performance.now() - start)}ms`)
86
86
  }
87
87
  }
88
88
 
@@ -97,7 +97,7 @@ class MainStore extends Store {
97
97
  data = me.generateData(value, me.amountColumns),
98
98
  start = performance.now();
99
99
 
100
- console.log('Start creating records');
100
+ console.log('Start generating data and adding to collection');
101
101
 
102
102
  if (me.items?.length > 0) {
103
103
  me.clear()
@@ -105,7 +105,7 @@ class MainStore extends Store {
105
105
 
106
106
  me.add(data);
107
107
 
108
- console.log(`Record creation total time: ${Math.round(performance.now() - start)}ms`)
108
+ console.log(`Data generation and collection add total time: ${Math.round(performance.now() - start)}ms`)
109
109
  }
110
110
 
111
111
  /**
@@ -47,7 +47,7 @@ class MainContainerStateProvider extends StateProvider {
47
47
 
48
48
  // if the main table store is already loaded, the country field renderer had no data
49
49
  if (mainStore.getCount() > 0) {
50
- mainStore.items.forEach(record => {
50
+ mainStore.forEach(record => {
51
51
  country = record.country;
52
52
 
53
53
  // hack resetting the current value to get a new record change
@@ -163,7 +163,7 @@ class EditUserDialog extends Dialog {
163
163
  me.record.set({annotations: {selected: false}})
164
164
  } else {
165
165
  // Assuming we want to support a single row selection
166
- store.items.forEach(record => {
166
+ store.forEach(record => {
167
167
  record.set({annotations: {
168
168
  selected: record === me.record ? data.value : false
169
169
  }})
@@ -71,7 +71,7 @@ class ViewportController extends Component {
71
71
 
72
72
  // if the main table store is already loaded, the country field renderer had no data
73
73
  if (mainStore.getCount() > 0) {
74
- mainStore.items.forEach(record => {
74
+ mainStore.forEach(record => {
75
75
  country = record.country;
76
76
 
77
77
  // hack resetting the current value to get a new record change
@@ -47,7 +47,7 @@ class MainContainerStateProvider extends StateProvider {
47
47
 
48
48
  // if the main table store is already loaded, the country field renderer had no data
49
49
  if (mainStore.getCount() > 0) {
50
- mainStore.items.forEach(record => {
50
+ mainStore.forEach(record => {
51
51
  country = record.country;
52
52
 
53
53
  // hack resetting the current value to get a new record change
@@ -145,7 +145,7 @@ class EditUserDialog extends Dialog {
145
145
  me.record.set({annotations: {selected: false}})
146
146
  } else {
147
147
  // Assuming we want to support a single row selection
148
- store.items.forEach(record => {
148
+ store.forEach(record => {
149
149
  record.set({annotations: {
150
150
  selected: record === me.record ? data.value : false
151
151
  }})
@@ -47,7 +47,7 @@ class ViewportStateProvider extends StateProvider {
47
47
 
48
48
  // if the main table store is already loaded, the country field renderer had no data
49
49
  if (mainStore.getCount() > 0) {
50
- mainStore.items.forEach(record => {
50
+ mainStore.forEach(record => {
51
51
  country = record.country;
52
52
 
53
53
  // hack resetting the current value to get a new record change
@@ -39,7 +39,7 @@ class MainContainer extends Viewport {
39
39
  handler() {
40
40
  let tabContainer = Neo.getComponent('myTableStoreContainer'),
41
41
  store = tabContainer.store,
42
- record = store.items[0];
42
+ record = store.getAt(0);
43
43
 
44
44
  record.firstname = record.firstname + '<span style="color:red;"> Foo</span>';
45
45
  }
@@ -61,7 +61,7 @@ class MainContainer extends Viewport {
61
61
 
62
62
  for (; j < repeats; j++) {
63
63
  for (i=0; i < countRecords; i++) {
64
- record = store.items[i];
64
+ record = store.getAt(i);
65
65
 
66
66
  Object.entries(record.toJSON()).forEach(([field, value]) => {
67
67
  if (field !== 'githubId') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "10.4.0",
3
+ "version": "10.5.0",
4
4
  "description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.4.0'
302
+ * @default '10.5.0'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.4.0'
307
+ version: '10.5.0'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -849,7 +849,7 @@ class Collection extends Base {
849
849
  * @returns {Object}
850
850
  */
851
851
  first() {
852
- return this._items[0]
852
+ return this.getAt(0)
853
853
  }
854
854
 
855
855
  /**
@@ -1041,7 +1041,7 @@ class Collection extends Base {
1041
1041
  * @returns {Object}
1042
1042
  */
1043
1043
  last() {
1044
- return this._items[this.count -1]
1044
+ return this.getAt(this.count -1)
1045
1045
  }
1046
1046
 
1047
1047
  /**
@@ -1171,6 +1171,15 @@ class Collection extends Base {
1171
1171
  return this._items.some(...args)
1172
1172
  }
1173
1173
 
1174
+ /**
1175
+ * Executes a provided function once for each array element.
1176
+ * @param {Function} fn The function to execute for each element.
1177
+ * @param {Object} [scope] Value to use as `this` when executing `fn`.
1178
+ */
1179
+ forEach(fn, scope) {
1180
+ this._items.forEach(fn, scope);
1181
+ }
1182
+
1174
1183
  /**
1175
1184
  * Removes items from and/or adds items to this collection
1176
1185
  * If the toRemoveArray is used, then the index is not used for removing, the entries are found by key and removed from where they are.
@@ -1214,10 +1223,19 @@ class Collection extends Base {
1214
1223
  }
1215
1224
  }
1216
1225
  } else if (removeCountAtIndex && removeCountAtIndex > 0) {
1217
- removedItems.push(...items.splice(index, removeCountAtIndex));
1218
- removedItems.forEach(e => {
1219
- map.delete(e[keyProperty])
1220
- })
1226
+ // Optimization: If this is a full clear operation, use map.clear()
1227
+ if (index === 0 && removeCountAtIndex === me.count) {
1228
+ removedItems = items;
1229
+ me._items = [];
1230
+ map.clear()
1231
+ } else {
1232
+ removedItems = items.splice(index, removeCountAtIndex);
1233
+
1234
+ // For partial removals, iterate and delete individual items from the map
1235
+ removedItems.forEach(e => {
1236
+ map.delete(e[keyProperty])
1237
+ })
1238
+ }
1221
1239
  }
1222
1240
 
1223
1241
  if (toAddArray && (len = toAddArray.length) > 0) {
@@ -307,7 +307,7 @@ class Gallery extends Component {
307
307
  if (Neo.isBoolean(oldValue)) {
308
308
  let me = this,
309
309
  i = 0,
310
- len = Math.min(me.maxItems, me.store.items.length),
310
+ len = Math.min(me.maxItems, me.store.count),
311
311
  view = me.getItemsRoot();
312
312
 
313
313
  if (me.vnodeInitialized) {
@@ -424,7 +424,7 @@ class Gallery extends Component {
424
424
  vdom = me.vdom,
425
425
  itemsRoot = me.getItemsRoot(),
426
426
  i = startIndex || 0,
427
- len = Math.min(me.maxItems, me.store.items.length),
427
+ len = Math.min(me.maxItems, me.store.count),
428
428
  amountColumns, item, vdomItem;
429
429
 
430
430
  if (orderByRow) {
@@ -432,7 +432,7 @@ class Gallery extends Component {
432
432
  }
433
433
 
434
434
  for (; i < len; i++) {
435
- item = me.store.items[i];
435
+ item = me.store.getAt(i);
436
436
  vdomItem = me.createItem(me.itemTpl, item, i);
437
437
 
438
438
  vdomItem. style = vdomItem.style || {};
@@ -581,7 +581,7 @@ class Helix extends Component {
581
581
  {deltaY, itemAngle, matrix, radius, rotationAngle, translateX, translateY, translateZ, vdom} = me,
582
582
  group = me.getItemsRoot(),
583
583
  i = startIndex || 0,
584
- len = Math.min(me.maxItems, me.store.items.length),
584
+ len = Math.min(me.maxItems, me.store.count),
585
585
  angle, item, matrixItems, transformStyle, vdomItem, c, s, x, y, z;
586
586
 
587
587
  if (!me.mounted) {
@@ -592,7 +592,7 @@ class Helix extends Component {
592
592
  }, me, {once: true})
593
593
  } else {
594
594
  for (; i < len; i++) {
595
- item = me.store.items[i];
595
+ item = me.store.getAt(i);
596
596
 
597
597
  angle = -rotationAngle + i * itemAngle;
598
598
 
@@ -952,7 +952,7 @@ class Helix extends Component {
952
952
  }
953
953
 
954
954
  for (; index < len; index++) {
955
- item = me.store.items[index];
955
+ item = me.store.getAt(index);
956
956
  vdomItem = vdom.cn[0].cn[0].cn[index];
957
957
 
958
958
  angle = -rotationAngle + index * itemAngle;
@@ -1024,7 +1024,7 @@ class Helix extends Component {
1024
1024
  for (; i < len; i++) {
1025
1025
  deltas.push({
1026
1026
  action: 'moveNode',
1027
- id : me.getItemVnodeId(me.store.items[i][me.keyProperty]),
1027
+ id : me.getItemVnodeId(me.store.getAt(i)[me.keyProperty]),
1028
1028
  index : i,
1029
1029
  parentId
1030
1030
  })
@@ -287,7 +287,7 @@ class GoogleMaps extends Base {
287
287
  windowId
288
288
  });
289
289
 
290
- this.markerStore.items.forEach(item => {
290
+ this.markerStore.forEach(item => {
291
291
  Neo.main.addon.GoogleMaps.addMarker({
292
292
  appName,
293
293
  mapId: id,
@@ -65,6 +65,11 @@ class Store extends Base {
65
65
  * @reactive
66
66
  */
67
67
  initialData_: null,
68
+ /**
69
+ * The initial chunk size for adding large datasets. Set to 0 to disable chunking.
70
+ * @member {Number} initialChunkSize=0
71
+ */
72
+ initialChunkSize: 0,
68
73
  /**
69
74
  * @member {Boolean} isGrouped=false
70
75
  */
@@ -140,31 +145,37 @@ class Store extends Base {
140
145
  */
141
146
  add(item) {
142
147
  let items = Array.isArray(item) ? item : [item];
143
- const threshold = 1000;
148
+ const threshold = this.initialChunkSize;
144
149
 
145
- if (items.length > threshold) {
150
+ if (threshold > 0 && items.length > threshold) {
146
151
  const me = this,
152
+ total = me.count + items.length,
147
153
  chunk = items.splice(0, threshold);
148
154
 
149
- // 1. Add the first chunk. This fires 'mutate' -> 'load' and triggers the initial grid render.
150
- super.add(me.createRecord(chunk));
155
+ me.chunkingTotal = total;
156
+
157
+ // 1. Add the first chunk. This fires 'mutate' -> 'load' (via onCollectionMutate)
158
+ // and triggers the initial grid render. The 'load' event will contain the final total count.
159
+ super.add(chunk); // Pass raw chunk directly
151
160
 
152
161
  // 2. Suspend events to prevent the next 'add' from firing 'load'.
153
162
  me.suspendEvents = true;
154
163
 
155
164
  // 3. Add the rest of the items silently.
156
- super.add(me.createRecord(items));
165
+ super.add(items); // Pass raw items directly
157
166
 
158
167
  // 4. Resume events.
159
168
  me.suspendEvents = false;
160
169
 
161
- // 5. Manually fire a final 'load' event to update the grid's scrollbar.
162
- me.fire('load', me.items);
170
+ // 5. Manually fire a final 'load' event to update the grid's scrollbar and notify other listeners.
171
+ me.fire('load', {items: me.items, postChunkLoad: true, total: me.chunkingTotal});
172
+
173
+ delete me.chunkingTotal;
163
174
 
164
- return me.count
175
+ return me.count;
165
176
  }
166
177
 
167
- return super.add(this.createRecord(item))
178
+ return super.add(item); // Pass raw item directly
168
179
  }
169
180
 
170
181
  /**
@@ -294,7 +305,7 @@ class Store extends Base {
294
305
  if (value) {
295
306
  this.isLoading = true;
296
307
 
297
- value = this.createRecord(value)
308
+ // value = this.createRecord(value)
298
309
  }
299
310
 
300
311
  return value
@@ -357,6 +368,88 @@ class Store extends Base {
357
368
  return isArray ? config : config[0]
358
369
  }
359
370
 
371
+ /**
372
+ * Overrides collection.Base:find() to ensure the returned item(s) are Record instances.
373
+ * @param {Object|String} property
374
+ * @param {String|Number} [value] Only required in case the first param is a string
375
+ * @param {Boolean} returnFirstMatch=false
376
+ * @returns {Object|Object[]|null}
377
+ */
378
+ find(property, value, returnFirstMatch=false) {
379
+ const result = super.find(property, value, returnFirstMatch);
380
+
381
+ if (returnFirstMatch) {
382
+ return result ? this.get(result[this.keyProperty]) : null;
383
+ } else {
384
+ return result.map(item => this.get(item[this.keyProperty]));
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Overrides collection.Base:findBy() to ensure the returned item(s) are Record instances.
390
+ * @param {function} fn The function to run for each item inside the start-end range. Return true for a match.
391
+ * @param {Object} scope=this The scope in which the passed function gets executed
392
+ * @param {Number} start=0 The start index
393
+ * @param {Number} end=this.count The end index (up to, last value excluded)
394
+ * @returns {Array}
395
+ */
396
+ findBy(fn, scope=this, start=0, end=this.count) {
397
+ const result = super.findBy(fn, scope, start, end);
398
+ return result.map(item => this.get(item[this.keyProperty]));
399
+ }
400
+
401
+ /**
402
+ * Overrides collection.Base:forEach() to ensure the iterated item is a Record instance.
403
+ * @param {Function} fn The function to execute for each record.
404
+ * @param {Object} [scope] Value to use as `this` when executing `fn`.
405
+ */
406
+ forEach(fn, scope) {
407
+ const me = this;
408
+ for (let i = 0; i < me.count; i++) {
409
+ fn.call(scope || me, me.getAt(i), i, me.items);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Overrides collection.Base:get() to ensure the returned item is a Record instance.
415
+ * @param {Number|String} key
416
+ * @returns {Object|null}
417
+ */
418
+ get(key) {
419
+ let item = super.get(key); // Get item from Collection.Base (could be raw data)
420
+
421
+ if (item && !RecordFactory.isRecord(item)) {
422
+ const record = RecordFactory.createRecord(this.model, item);
423
+ // Replace the raw data with the record instance in the collection
424
+ this.map.set(key, record);
425
+ const index = this._items.indexOf(item); // Find the index of the raw item
426
+ if (index !== -1) {
427
+ this._items[index] = record; // Replace it with the record
428
+ }
429
+ return record;
430
+ }
431
+ return item; // Already a record or null
432
+ }
433
+
434
+ /**
435
+ * Overrides collection.Base:getAt() to ensure the returned item is a Record instance.
436
+ * @param {Number} index
437
+ * @returns {Object|undefined}
438
+ */
439
+ getAt(index) {
440
+ let item = super.getAt(index); // Get item from Collection.Base (could be raw data)
441
+
442
+ if (item && !RecordFactory.isRecord(item)) {
443
+ const record = RecordFactory.createRecord(this.model, item);
444
+ // Replace the raw data with the record instance in the collection
445
+ this._items[index] = record;
446
+ // Also update the map, as the key might be derived from the item
447
+ this.map.set(record[this.keyProperty], record);
448
+ return record;
449
+ }
450
+ return item; // Already a record or undefined
451
+ }
452
+
360
453
  /**
361
454
  * @returns {String}
362
455
  */
@@ -447,7 +540,7 @@ class Store extends Base {
447
540
  let me = this;
448
541
 
449
542
  if (me.isConstructed && !me.isLoading) {
450
- me.fire('load', me.items)
543
+ me.fire('load', {items: me.items, total: me.chunkingTotal});
451
544
  }
452
545
  }
453
546
 
@@ -478,7 +571,7 @@ class Store extends Base {
478
571
  // => break the sync flow to ensure potential listeners got applied
479
572
  Promise.resolve().then(() => {
480
573
  if (me.isLoaded) {
481
- me.fire('load', me.items)
574
+ me.fire('load', {items: me.items})
482
575
  } else if (me.autoLoad) {
483
576
  me.load()
484
577
  }
@@ -62,7 +62,7 @@ class DragZone extends BaseDragZone {
62
62
  {store} = owner,
63
63
  node;
64
64
 
65
- store.items.forEach((record, index) => {
65
+ store.forEach((record, index) => {
66
66
  node = me.getItemVdom(record, index);
67
67
 
68
68
  if (node) {
@@ -45,7 +45,7 @@ class DragZone extends BaseDragZone {
45
45
  {store} = owner,
46
46
  node;
47
47
 
48
- store.items.forEach(record => {
48
+ store.forEach(record => {
49
49
  if (!record.isLeaf) {
50
50
  node = owner.getVdomChild(owner.getItemId(record.id), owner.vdom);
51
51
  node.cls = node.cls || [];
@@ -695,7 +695,7 @@ class ComboBox extends Picker {
695
695
  updateInputValueFromValue(value) {
696
696
  let inputValue = null;
697
697
 
698
- if (Neo.isRecord(value)) {
698
+ if (Neo.isObject(value) || Neo.isRecord(value)) {
699
699
  inputValue = value[this.displayField]
700
700
  }
701
701
 
@@ -745,7 +745,15 @@ class ComboBox extends Picker {
745
745
  if (me.typeAhead) {
746
746
  if (!me.value && value?.length > 0) {
747
747
  const search = value.toLocaleLowerCase();
748
- match = store.items.find(r => r[displayField]?.toLowerCase?.()?.startsWith(search));
748
+ let match = null;
749
+
750
+ for (let i = 0; i < store.count; i++) {
751
+ const r = store.getAt(i);
752
+ if (r[displayField]?.toLowerCase?.()?.startsWith(search)) {
753
+ match = r;
754
+ break;
755
+ }
756
+ }
749
757
 
750
758
  if (match && inputHintEl) {
751
759
  inputHintEl.value = value + match[displayField].substr(value.length);
package/src/grid/Body.mjs CHANGED
@@ -181,6 +181,17 @@ class GridBody extends Component {
181
181
  ]}
182
182
  }
183
183
 
184
+ /**
185
+ * Internal flag to adopt to store.add() passing an initial chunk.
186
+ * @member {Number} #initialChunkSize=0
187
+ */
188
+ #initialChunkSize = 0
189
+ /**
190
+ * Internal flag to adopt to store.add() passing an initial chunk.
191
+ * @member {Number} #initialChunkSize=0
192
+ */
193
+ #initialTotalSize = 0
194
+
184
195
  /**
185
196
  * @member {String[]} selectedCells
186
197
  */
@@ -427,6 +438,11 @@ class GridBody extends Component {
427
438
 
428
439
  oldValue?.un(listeners);
429
440
  value ?.on(listeners);
441
+
442
+ // Clear component instances when the store changes or is replaced
443
+ if (oldValue) {
444
+ me.clearComponentColumnMaps();
445
+ }
430
446
  }
431
447
 
432
448
  /**
@@ -580,6 +596,46 @@ class GridBody extends Component {
580
596
  return ClassSystemUtil.beforeSetInstance(value, RowModel)
581
597
  }
582
598
 
599
+ /**
600
+ * Destroys all component instances created by component columns.
601
+ * @protected
602
+ */
603
+ clearComponentColumnMaps() {
604
+ let me = this,
605
+ columns = me.parent.columns.items;
606
+
607
+ columns.forEach(column => {
608
+ if (column instanceof Neo.grid.column.Component) {
609
+ column.map.forEach(component => {
610
+ component.destroy()
611
+ });
612
+ column.map.clear()
613
+ }
614
+ });
615
+ }
616
+
617
+ /**
618
+ * Cleans up component instances that are no longer visible or needed.
619
+ * @protected
620
+ */
621
+ cleanupComponentInstances() {
622
+ let me = this;
623
+
624
+ me.parent.columns.items.forEach(column => {
625
+ if (column instanceof Neo.grid.column.Component) {
626
+ column.map.forEach((component, id) => {
627
+ // Extract rowIndex from component ID (e.g., "grid-body-1-component-950")
628
+ const componentRowIndex = parseInt(id.split('-').pop());
629
+
630
+ if (componentRowIndex < me.mountedRows[0] || componentRowIndex > me.mountedRows[1]) {
631
+ component.destroy();
632
+ column.map.delete(id)
633
+ }
634
+ });
635
+ }
636
+ });
637
+ }
638
+
583
639
  /**
584
640
  * @param {Object} opts
585
641
  * @param {Object} opts.record
@@ -618,7 +674,7 @@ class GridBody extends Component {
618
674
 
619
675
  style: {
620
676
  height : me.rowHeight + 'px',
621
- transform: `translate(0px, ${rowIndex * me.rowHeight}px)`
677
+ transform: `translate3d(0px, ${rowIndex * me.rowHeight}px, 0px)`
622
678
  }
623
679
  };
624
680
 
@@ -662,7 +718,7 @@ class GridBody extends Component {
662
718
  let me = this,
663
719
  {mountedRows, store} = me,
664
720
  rows = [],
665
- i;
721
+ endIndex, i, range;
666
722
 
667
723
  if (
668
724
  store.isLoading ||
@@ -674,18 +730,24 @@ class GridBody extends Component {
674
730
  return
675
731
  }
676
732
 
677
- // Creates the new start & end indexes
678
- me.updateMountedAndVisibleRows();
733
+ if (me.#initialChunkSize > 0) {
734
+ endIndex = me.#initialChunkSize;
735
+ range = endIndex;
736
+ } else {
737
+ // Creates the new start & end indexes
738
+ me.updateMountedAndVisibleRows();
739
+ endIndex = mountedRows[1]
740
+ }
679
741
 
680
- for (i=mountedRows[0]; i < mountedRows[1]; i++) {
681
- rows.push(me.createRow({record: store.items[i], rowIndex: i}))
742
+ for (i=mountedRows[0]; i < endIndex; i++) {
743
+ rows.push(me.createRow({record: store.getAt(i), rowIndex: i}))
682
744
  }
683
745
 
684
746
  me.getVdomRoot().cn = rows;
685
747
 
686
748
  me.parent.isLoading = false;
687
749
 
688
- me.updateScrollHeight(true); // silent
750
+ me.updateScrollHeight(true, range); // silent
689
751
  !silent && me.update()
690
752
  }
691
753
 
@@ -694,6 +756,7 @@ class GridBody extends Component {
694
756
  */
695
757
  destroy(...args) {
696
758
  this.store = null; // remove the listeners
759
+ this.clearComponentColumnMaps(); // Destroy component instances
697
760
 
698
761
  super.destroy(...args)
699
762
  }
@@ -862,7 +925,11 @@ class GridBody extends Component {
862
925
  getRowId(rowIndex) {
863
926
  let me = this;
864
927
 
865
- return `${me.id}__row-${rowIndex % (me.availableRows + 2 * me.bufferRowRange)}`
928
+ if (me.#initialChunkSize > 0) {
929
+ return `${me.id}__row-${rowIndex}`
930
+ } else {
931
+ return `${me.id}__row-${rowIndex % (me.availableRows + 2 * me.bufferRowRange)}`
932
+ }
866
933
  }
867
934
 
868
935
  /**
@@ -932,10 +999,13 @@ class GridBody extends Component {
932
999
  }
933
1000
 
934
1001
  /**
935
- * @param {Object[]} data
1002
+ * @param {Object} data
1003
+ * @param {Object[]} data.items
1004
+ * @param {Boolean} [data.postChunkLoad]
1005
+ * @param {Number} [data.total]
936
1006
  * @protected
937
1007
  */
938
- onStoreLoad(data) {
1008
+ onStoreLoad({items, postChunkLoad, total}) {
939
1009
  let me = this;
940
1010
 
941
1011
  /*
@@ -944,7 +1014,7 @@ class GridBody extends Component {
944
1014
  * This logic bypasses the standard update() cycle by directly clearing the vdom,
945
1015
  * vnode cache and the real DOM via textContent.
946
1016
  */
947
- if (data?.length < 1) {
1017
+ if (items?.length < 1) {
948
1018
  const vdomRoot = me.getVdomRoot();
949
1019
 
950
1020
  // No change, opt out
@@ -963,9 +1033,19 @@ class GridBody extends Component {
963
1033
  return
964
1034
  }
965
1035
 
966
- me.createViewData();
1036
+ // If it's the first chunked load (data.total exists and data.items is a subset of total)
1037
+ // Render the entire chunk for immediate scrollability
1038
+ if (total && items.length < total) {
1039
+ me.#initialChunkSize = items.length;
1040
+ me.#initialTotalSize = total;
1041
+ me.createViewData();
1042
+ me.#initialChunkSize = 0
1043
+ me.#initialTotalSize = 0
1044
+ } else {
1045
+ me.createViewData()
1046
+ }
967
1047
 
968
- if (me.mounted) {
1048
+ if (me.mounted && !postChunkLoad) {
969
1049
  me.timeout(50).then(() => {
970
1050
  Neo.main.DomAccess.scrollTo({
971
1051
  direction: 'top',
@@ -974,6 +1054,11 @@ class GridBody extends Component {
974
1054
  })
975
1055
  })
976
1056
  }
1057
+
1058
+ // Cleanup component instances after chunked load
1059
+ if (postChunkLoad) {
1060
+ me.cleanupComponentInstances()
1061
+ }
977
1062
  }
978
1063
 
979
1064
  /**
@@ -1172,7 +1257,7 @@ class GridBody extends Component {
1172
1257
  */
1173
1258
  updateScrollHeight(silent=false) {
1174
1259
  let me = this,
1175
- countRecords = me.store?.getCount() || 0,
1260
+ countRecords = me.#initialTotalSize || me.store?.count || 0,
1176
1261
  {rowHeight} = me;
1177
1262
 
1178
1263
  if (countRecords > 0 && rowHeight > 0) {
@@ -565,13 +565,16 @@ class GridContainer extends BaseContainer {
565
565
  }
566
566
 
567
567
  /**
568
- * @param {Object[]} data
568
+ * @param {Object} data
569
+ * @param {Object[]} data.items
570
+ * @param {Number} [data.total]
569
571
  * @protected
570
572
  */
571
573
  onStoreLoad(data) {
572
- let me = this;
574
+ let me = this,
575
+ totalCount = data.total ? data.total : this.store.count;
573
576
 
574
- me.updateRowCount();
577
+ me.updateRowCount(totalCount);
575
578
 
576
579
  if (me.store.sorters?.length < 1) {
577
580
  me.removeSortingCss()
@@ -664,13 +667,15 @@ class GridContainer extends BaseContainer {
664
667
  }
665
668
 
666
669
  /**
667
- * @param {Boolean} silent=false
670
+ * @param {Number} [count] The total number of rows in the store. Optional, will use store.count if not provided.
671
+ * @param {Boolean} [silent=false]
668
672
  */
669
- updateRowCount(silent=false) {
670
- let me = this;
673
+ updateRowCount(count, silent=false) {
674
+ let me = this,
675
+ finalCount = count ? count : me.store.count;
671
676
 
672
- this.getVdomRoot()['aria-rowcount'] = me.store.getCount() + 2; // 1 based & the header row counts as well
673
- !silent && this.update()
677
+ me.getVdomRoot()['aria-rowcount'] = finalCount + 2;
678
+ !silent && me.update()
674
679
  }
675
680
  }
676
681
 
@@ -100,18 +100,19 @@ class VerticalScrollbar extends Component {
100
100
  filter: me.updateScrollHeight,
101
101
  load : me.updateScrollHeight,
102
102
  scope : me
103
- });
104
-
105
- value.getCount() > 0 && me.updateScrollHeight()
103
+ })
106
104
  }
107
105
  }
108
106
 
109
107
  /**
110
- *
108
+ * @param {Object} data
109
+ * @param {Object[]} data.items
110
+ * @param {Number} [data.total]
111
+ * @protected
111
112
  */
112
- updateScrollHeight() {
113
+ updateScrollHeight(data) {
113
114
  let me = this,
114
- countRecords = me.store.getCount(),
115
+ countRecords = data?.total ? data.total : me.store.count,
115
116
  {rowHeight} = me;
116
117
 
117
118
  if (countRecords > 0 && rowHeight > 0) {
@@ -132,9 +132,14 @@ class Component extends Column {
132
132
  */
133
133
  getComponentId(rowIndex) {
134
134
  let me = this,
135
- {body} = me.parent;
135
+ {body} = me.parent,
136
+ store = body.store; // Access the store from the body
136
137
 
137
- return `${me.id}-component-${rowIndex % (body.availableRows + 2 * body.bufferRowRange)}`
138
+ if (store.chunkingTotal) { // Check if chunking is active
139
+ return `${me.id}-component-${rowIndex}`; // Use rowIndex directly
140
+ } else {
141
+ return `${me.id}-component-${rowIndex % (body.availableRows + 2 * body.bufferRowRange)}`
142
+ }
138
143
  }
139
144
  }
140
145
 
package/src/list/Base.mjs CHANGED
@@ -609,7 +609,7 @@ class List extends Component {
609
609
  if (!(me.animate && !me.getPlugin('list-animate'))) {
610
610
  vdom.cn = [];
611
611
 
612
- me.store.items.forEach((item, index) => {
612
+ me.store.forEach((item, index) => {
613
613
  listItem = me.createItem(item, index);
614
614
  listItem && vdom.cn.push(listItem)
615
615
  });
@@ -29,7 +29,9 @@ class TreeAccordionModel extends TreeModel {
29
29
  recordId = record[view.getKeyProperty()],
30
30
  childRecord = null;
31
31
 
32
- for (const item of view.store.items) {
32
+ for (let i = 0; i < view.store.count; i++) {
33
+ const item = view.store.getAt(i);
34
+
33
35
  if (item.parentId === recordId) {
34
36
  childRecord = item;
35
37
  break
@@ -66,7 +68,9 @@ class TreeAccordionModel extends TreeModel {
66
68
  nextItemRecord = null,
67
69
  previousItemRecord;
68
70
 
69
- for (let item of store.items) {
71
+ for (let i = 0; i < store.count; i++) {
72
+ const item = store.getAt(i);
73
+
70
74
  if (hasFoundNext && item.parentId === parentRecordId) {
71
75
  nextItemRecord = item;
72
76
  break
@@ -255,7 +259,9 @@ class TreeAccordionModel extends TreeModel {
255
259
  {store} = view,
256
260
  record, rootItemId;
257
261
 
258
- for (record of store.items) {
262
+ for (let i = 0; i < store.count; i++) {
263
+ const record = store.getAt(i);
264
+
259
265
  if (!record.parentId) {
260
266
  rootItemId = view.getItemId(record[view.getKeyProperty()]);
261
267
  break
@@ -357,7 +357,7 @@ class TableBody extends Component {
357
357
  rows = [];
358
358
 
359
359
  for (; i < countRecords; i++) {
360
- rows.push(me.createRow({record: store.items[i], rowIndex: i}))
360
+ rows.push(me.createRow({record: store.getAt(i), rowIndex: i}))
361
361
  }
362
362
 
363
363
  me.vdom.cn = rows;
@@ -558,12 +558,39 @@ class TableBody extends Component {
558
558
  }
559
559
 
560
560
  /**
561
- * @param {Object[]} data
561
+ * @param {Object} data
562
+ * @param {Object[]} data.items
563
+ * @param {Number} [data.total]
562
564
  * @protected
563
565
  */
564
566
  onStoreLoad(data) {
565
567
  let me = this;
566
568
 
569
+ /*
570
+ * Fast path to handle clearing all rows (e.g., store.removeAll()).
571
+ * A full vdom diff against all existing rows is a performance bottleneck.
572
+ * This logic bypasses the standard update() cycle by directly clearing the vdom,
573
+ * vnode cache and the real DOM via textContent.
574
+ */
575
+ if (data?.items.length < 1) {
576
+ const vdomRoot = me.getVdomRoot();
577
+
578
+ // No change, opt out
579
+ if (vdomRoot.cn.length < 1) {
580
+ return
581
+ }
582
+
583
+ vdomRoot.cn = [];
584
+ me.getVnodeRoot().childNodes = [];
585
+
586
+ Neo.applyDeltas(me.appName, {
587
+ id : vdomRoot.id,
588
+ textContent: ''
589
+ });
590
+
591
+ return
592
+ }
593
+
567
594
  me.createViewData();
568
595
 
569
596
  if (me.mounted) {
@@ -133,7 +133,7 @@ class AccordionTree extends TreeList {
133
133
  if (me.vnodeInitialized && value === false) {
134
134
  let {store} = me;
135
135
 
136
- store.items.forEach(record => {
136
+ store.forEach(record => {
137
137
  if (record.parentId === null && !record.isLeaf) {
138
138
  me.expandItem(record)
139
139
  }
@@ -153,7 +153,7 @@ class AccordionTree extends TreeList {
153
153
  {store} = me,
154
154
  hide = !value;
155
155
 
156
- store.items.forEach((record) => {
156
+ store.forEach(record => {
157
157
  const itemId = me.getItemId(record[me.getKeyProperty()]),
158
158
  vdom = me.getVdomChild(itemId),
159
159
  itemVdom = VDomUtil.getByFlag(vdom, 'iconCls');
package/src/tree/List.mjs CHANGED
@@ -162,7 +162,7 @@ class Tree extends Base {
162
162
  hasMatch = false,
163
163
  node;
164
164
 
165
- me.store.items.forEach(item => {
165
+ me.store.forEach(item => {
166
166
  if (!item.isLeaf) {
167
167
  node = me.getVdomChild(me.getItemId(item.id), me.vdom);
168
168
 
@@ -303,7 +303,7 @@ class Tree extends Base {
303
303
  hasMatch = false,
304
304
  node;
305
305
 
306
- me.store.items.forEach(item => {
306
+ me.store.forEach(item => {
307
307
  if (!item.isLeaf) {
308
308
  node = me.getVdomChild(me.getItemId(item.id), me.vdom);
309
309
 
@@ -337,7 +337,7 @@ class Tree extends Base {
337
337
  value = ''
338
338
  }
339
339
 
340
- me.store.items.forEach(item => {
340
+ me.store.forEach(item => {
341
341
  if (item.parentId === parentId) {
342
342
  directMatch = false;
343
343
  node = me.getVdomChild(me.getItemId(item.id), me.vdom);